# Pullbay

Fast, clean, and stable access to public web data

<h2 align="center">Data APIs that just work</h2>

<p align="center">Stop building scrapers. Start shipping products.<br>Pullbay gives you reliable access to public web data through simple API calls.</p>

<p align="center"><a href="https://dashboard.pullbay.com/register" class="button primary">Get started free</a> <a href="https://dashboard.pullbay.com/" class="button secondary">Sign in</a></p>

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>📚</strong></td><td><strong>Documentation</strong></td><td>Learn how to integrate Pullbay into your projects</td><td><a href="https://docs.pullbay.com/">https://docs.pullbay.com/</a></td></tr><tr><td><strong>🔌</strong></td><td><strong>API Reference</strong></td><td>Browse endpoints and test requests</td><td><a href="https://docs.pullbay.com/api-reference">https://docs.pullbay.com/api-reference</a></td></tr></tbody></table>

{% columns %}
{% column %}

#### Make your first call in minutes

No complex setup. No infrastructure to manage. Just API calls that return clean, structured data.

1. Create a free account
2. Get your API key
3. Make a request
4. Get JSON back

<a href="https://dashboard.pullbay.com/" class="button primary">Get started</a> <a href="https://docs.pullbay.com/api-reference" class="button secondary">View API reference</a>
{% endcolumn %}

{% column %}
{% code title="Example request" overflow="wrap" %}

```bash
curl "https://api.pullbay.com/appstore/reviews?app_id=284882215" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

{% endcode %}

{% code title="Response" overflow="wrap" %}

```json
{
  "data": [
    {
      "id": "10293847561",
      "author": "HappyUser",
      "rating": 5,
      "title": "Great app!",
      "content": "This app changed everything..."
    }
  ],
  "pagination": { "has_more": true }
}
```

{% endcode %}
{% endcolumn %}
{% endcolumns %}

***

<h2 align="center">Available data sources</h2>

<p align="center">Access structured data from popular platforms. New sources added regularly.</p>

<table data-view="cards"><thead><tr><th></th><th></th><th></th></tr></thead><tbody><tr><td><strong>📱</strong></td><td><strong>App Stores</strong></td><td>Reviews, ratings, and metadata from App Store and Google Play</td></tr><tr><td><strong>🔍</strong></td><td><strong>Search Engines</strong></td><td>Google Search (SERP), News, Maps, and more</td></tr><tr><td><strong>🎬</strong></td><td><strong>Social &#x26; Media</strong></td><td>YouTube, Instagram and other platforms</td></tr></tbody></table>

***

<h2 align="center">Why Pullbay?</h2>

{% columns %}
{% column %}

#### Battle-tested infrastructure

Every endpoint is continuously monitored, tested, and maintained. We handle proxy rotation, retry logic, and anti-bot detection so you don't have to.
{% endcolumn %}

{% column %}

#### Simple credit-based pricing

Pay only for the data you use. No monthly minimums, no surprise bills. Top up credits anytime and scale as needed.
{% endcolumn %}

{% column %}

#### Built for automation

Designed to work seamlessly with n8n, Make.com, and custom workflows. Managed pagination endpoints return all data in one request.
{% endcolumn %}
{% endcolumns %}

***

<h2 align="center">Need help?</h2>

<p align="center">We're here to help you get the most out of Pullbay.</p>

<p align="center">                                             <a href="https://pullbay.com/contact" class="button primary">Contact support</a></p>


# API Reference

Complete API reference for all Pullbay endpoints

## Authentication

Include your API key in the Authorization header:

```
Authorization: Bearer YOUR_API_KEY
```

Get your API key from the dashboard.

### Available services

| Service       | What you can access                                                          |
| ------------- | ---------------------------------------------------------------------------- |
| App Store     | iOS app lookup, search, similar apps, reviews, developer apps                |
| Google Play   | Android app lookup, search, similar apps, reviews, permissions, data safety  |
| Apartments    | Apartments.com properties, photos, and reviews                               |
| Google Maps   | Place search, place details, and reviews                                     |
| Google News   | Articles by category or search query                                         |
| Google Search | Web search results                                                           |
| DuckDuckGo    | Web search, images, news, videos                                             |
| Instagram     | Users, posts, reels, stories, and comments                                   |
| Reddit        | Posts, comments, users, and subreddits                                       |
| TikTok        | Users, posts, comments, hashtags, and search                                 |
| Twitter       | Tweets, users, handles, and search                                           |
| Yelp          | Places, reviews, and search                                                  |
| YouTube       | Channels, videos, shorts, live streams, comments, search, trending, hashtags |

Each service has its own reference page with full request/response schemas and examples.

### Quick example

```bash
# Fetch App Store reviews
curl "https://api.pullbay.com/appstore/reviews?app_id=284882215" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "10293847561",
      "author": "HappyUser",
      "rating": 5,
      "title": "Great app!",
      "content": "This app has changed how I work...",
      "date": "2024-01-15T10:30:00Z",
      "version": "5.2.1"
    },
    ...49 more items
  ],
  "pagination": { "page": 1, "hasNextPage": true, "cursor": null, "offset": null },
  "pricing": { "currency": "USD", "amountCharged": "0.010000", "billableItems": 50 }
}
```

This request costs **$0.01** for 50 billable items at **$0.0002 per item**.

### Response format

All endpoints return the same envelope:

* `requestId` — unique ID for the request, useful when contacting support
* `status` / `message` / `success` — HTTP status, status text, and a boolean success flag
* `data` — the result(s)
* `pagination` — appears on list and search endpoints (`page`, `hasNextPage`, `cursor`, `offset`)
* `pricing.currency` — the billing currency (`USD`).
* `pricing.amountCharged` — the amount charged, returned as a decimal string with six decimal places, such as `"0.010000"` for $0.01.
* `pricing.billableItems` — the number of items charged. This can exceed the number returned when an endpoint’s minimum charge applies.

An `error` object is returned on failure.

### Pagination

List and search endpoints support two mutually exclusive modes on the same request:

* `page` — fetch one page at a time (fine control)
* `maxItems` — a one-shot bulk pull that the API pages through internally (simpler for batch jobs and automation)

Detail endpoints (e.g. app lookup, place details) return a single resource or a fixed set in one call, with no pagination.

### Need help?

* [**Documentation**](https://docs.pullbay.com/documentation/) — Guides and tutorials


# Apartments

Apartments.com: properties, photos, and reviews.

## Search properties

> Search Apartments.com properties by location with optional rent/bed/bath filters. Use \`page\` (1-indexed) to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.0003 per item\
> \
> \*\*Minimum charge:\*\* $0.003 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"apartments","description":"Apartments.com: properties, photos, and reviews."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"ApartmentsProperty":{"type":"object","description":"An Apartments.com property summary (search result entry).","properties":{"id":{"type":"string"},"name":{"type":"string"},"bedRange":{"type":"string"},"rentRange":{"type":"string"},"address":{"type":"object","additionalProperties":true}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/apartments/properties":{"get":{"operationId":"searchApartmentsProperties","summary":"Search properties","description":"Search Apartments.com properties by location with optional rent/bed/bath filters. Use `page` (1-indexed) to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.0003 per item\n\n**Minimum charge:** $0.003 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["apartments"],"parameters":[{"name":"location","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":40}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["default","lastUpdated","rentHighToLow","rentLowToHigh"]}},{"name":"minRent","in":"query","required":false,"schema":{"type":"number","minimum":0,"maximum":999999999}},{"name":"maxRent","in":"query","required":false,"schema":{"type":"number","minimum":0,"maximum":999999999}},{"name":"minBed","in":"query","required":false,"schema":{"type":"number","minimum":0,"maximum":4}},{"name":"maxBed","in":"query","required":false,"schema":{"type":"number","minimum":0,"maximum":4}},{"name":"minBath","in":"query","required":false,"schema":{"type":"number","minimum":0,"maximum":3}},{"name":"maxBath","in":"query","required":false,"schema":{"type":"number","minimum":0,"maximum":3}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/ApartmentsProperty"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get a property by ID

> Returns full details for an Apartments.com property. Use the \`includeScore\`, \`includeModelDetails\`, and \`includeImages\` flags to opt into extra fields.\
> \
> \*\*Price:\*\* $0.0015 per item\
> \
> \*\*Minimum charge:\*\* $0.0015 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"apartments","description":"Apartments.com: properties, photos, and reviews."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"ApartmentsPropertyDetail":{"type":"object","description":"Full property detail returned by /properties/{propertyId}.","properties":{"id":{"type":"string"},"propertyName":{"type":"string"},"url":{"type":"string"},"propertyWebsite":{"type":"string"},"lastUpdated":{"type":"string"},"description":{"type":"string"},"rating":{"type":"number"},"beds":{"type":"string"},"baths":{"type":"string"},"sqft":{"type":"string"},"baseRent":{"type":"object","additionalProperties":true},"totalRent":{"type":"object","additionalProperties":true},"location":{"type":"object","additionalProperties":true},"coordinates":{"type":"object","properties":{"latitude":{"type":"number"},"longitude":{"type":"number"}}},"contact":{"type":"object","additionalProperties":true},"specials":{"type":"object","nullable":true,"additionalProperties":true},"schools":{"type":"object","additionalProperties":true},"amenities":{"type":"array","items":{"type":"object","additionalProperties":true}},"fees":{"type":"array","items":{"type":"object","additionalProperties":true}},"models":{"type":"array","items":{"type":"object","additionalProperties":true}},"rentals":{"type":"array","items":{"type":"object","additionalProperties":true}},"transportation":{"type":"array","items":{"type":"object","additionalProperties":true}},"transitAndPOI":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/apartments/properties/{propertyId}":{"get":{"operationId":"getApartmentsPropertyDetail","summary":"Get a property by ID","description":"Returns full details for an Apartments.com property. Use the `includeScore`, `includeModelDetails`, and `includeImages` flags to opt into extra fields.\n\n**Price:** $0.0015 per item\n\n**Minimum charge:** $0.0015 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["apartments"],"parameters":[{"name":"propertyId","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":20}},{"name":"includeScore","in":"query","required":false,"schema":{"type":"string","enum":["true","false"]}},{"name":"includeModelDetails","in":"query","required":false,"schema":{"type":"string","enum":["true","false"]}},{"name":"includeImages","in":"query","required":false,"schema":{"type":"string","enum":["true","false"]}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/ApartmentsPropertyDetail"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get images for a property

> Returns photos, floor plans, virtual tours, and extended virtual tours for a property.\
> \
> \*\*Price:\*\* $0.0003 per item\
> \
> \*\*Minimum charge:\*\* $0.003 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"apartments","description":"Apartments.com: properties, photos, and reviews."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"ApartmentsPropertyImages":{"type":"object","description":"Photos, floor plans, and virtual tours for a property.","properties":{"photos":{"type":"array","items":{"type":"object","additionalProperties":true}},"floorPlans":{"type":"array","items":{"type":"object","additionalProperties":true}},"virtualTours":{"type":"array","items":{"type":"object","additionalProperties":true}},"virtualTourExtended":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/apartments/properties/{propertyId}/images":{"get":{"operationId":"getApartmentsPropertyImages","summary":"Get images for a property","description":"Returns photos, floor plans, virtual tours, and extended virtual tours for a property.\n\n**Price:** $0.0003 per item\n\n**Minimum charge:** $0.003 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["apartments"],"parameters":[{"name":"propertyId","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":20}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/ApartmentsPropertyImages"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get reviews for a property

> Returns reviews for an Apartments.com property. Use \`page\` (1-indexed) to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.0003 per item\
> \
> \*\*Minimum charge:\*\* $0.003 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"apartments","description":"Apartments.com: properties, photos, and reviews."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"ApartmentsPropertyReview":{"type":"object","description":"A review for an Apartments.com property.","properties":{"key":{"type":"string"},"title":{"type":"string"},"text":{"type":"string"},"rating":{"type":"number"},"submissionDate":{"type":"string"},"isOwner":{"type":"boolean"},"positiveFeedbackCount":{"type":"integer"},"comments":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/apartments/properties/{propertyId}/reviews":{"get":{"operationId":"getApartmentsPropertyReviews","summary":"Get reviews for a property","description":"Returns reviews for an Apartments.com property. Use `page` (1-indexed) to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.0003 per item\n\n**Minimum charge:** $0.003 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["apartments"],"parameters":[{"name":"propertyId","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":20}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/ApartmentsPropertyReview"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```


# Appstore

Apple App Store: app lookup, search, similar apps, reviews, developer apps.

## Get a single application by App ID or Bundle ID

> Returns one application. Provide exactly one of \`appId\` or \`bundleId\`.\
> \
> \*\*Price:\*\* $0.0002 per item\
> \
> \*\*Minimum charge:\*\* $0.002 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"appstore","description":"Apple App Store: app lookup, search, similar apps, reviews, developer apps."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"Application":{"type":"object","properties":{"id":{"type":"integer"},"appId":{"type":"string"},"title":{"type":"string"},"url":{"type":"string","format":"uri"},"description":{"type":"string"},"icon":{"type":"string","format":"uri"},"genres":{"type":"array","items":{"type":"string"}},"genreIds":{"type":"array","items":{"type":"string"}},"primaryGenre":{"type":"string"},"primaryGenreId":{"type":"integer"},"contentRating":{"type":"string"},"languages":{"type":"array","items":{"type":"string"}},"size":{"type":"string"},"requiredOsVersion":{"type":"string"},"released":{"type":"string","format":"date-time"},"updated":{"type":"string","format":"date-time"},"releaseNotes":{"type":"string"},"version":{"type":"string"},"price":{"type":"number"},"currency":{"type":"string"},"free":{"type":"boolean"},"developerId":{"type":"integer"},"developer":{"type":"string"},"developerUrl":{"type":"string","format":"uri"},"developerWebsite":{"type":"string","format":"uri"},"score":{"type":"number"},"reviews":{"type":"integer"},"currentVersionScore":{"type":"number"},"currentVersionReviews":{"type":"integer"},"screenshots":{"type":"array","items":{"type":"string","format":"uri"}},"ipadScreenshots":{"type":"array","items":{"type":"string","format":"uri"}},"appletvScreenshots":{"type":"array","items":{"type":"string","format":"uri"}},"supportedDevices":{"type":"array","items":{"type":"string"}}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/appstore/apps":{"get":{"operationId":"getApp","summary":"Get a single application by App ID or Bundle ID","description":"Returns one application. Provide exactly one of `appId` or `bundleId`.\n\n**Price:** $0.0002 per item\n\n**Minimum charge:** $0.002 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["appstore"],"parameters":[{"name":"appId","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{8,12}$"}},{"name":"bundleId","in":"query","required":false,"schema":{"type":"string","maxLength":255,"pattern":"^[A-Za-z0-9.]+$"}},{"name":"country","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":2}},{"name":"lang","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2,3}(-[a-z]{2,3})?$"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Application"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Search applications by term

> Search the App Store for applications matching \`term\`. Use \`page\` for pagination or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.0002 per item\
> \
> \*\*Minimum charge:\*\* $0.002 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"appstore","description":"Apple App Store: app lookup, search, similar apps, reviews, developer apps."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"Application":{"type":"object","properties":{"id":{"type":"integer"},"appId":{"type":"string"},"title":{"type":"string"},"url":{"type":"string","format":"uri"},"description":{"type":"string"},"icon":{"type":"string","format":"uri"},"genres":{"type":"array","items":{"type":"string"}},"genreIds":{"type":"array","items":{"type":"string"}},"primaryGenre":{"type":"string"},"primaryGenreId":{"type":"integer"},"contentRating":{"type":"string"},"languages":{"type":"array","items":{"type":"string"}},"size":{"type":"string"},"requiredOsVersion":{"type":"string"},"released":{"type":"string","format":"date-time"},"updated":{"type":"string","format":"date-time"},"releaseNotes":{"type":"string"},"version":{"type":"string"},"price":{"type":"number"},"currency":{"type":"string"},"free":{"type":"boolean"},"developerId":{"type":"integer"},"developer":{"type":"string"},"developerUrl":{"type":"string","format":"uri"},"developerWebsite":{"type":"string","format":"uri"},"score":{"type":"number"},"reviews":{"type":"integer"},"currentVersionScore":{"type":"number"},"currentVersionReviews":{"type":"integer"},"screenshots":{"type":"array","items":{"type":"string","format":"uri"}},"ipadScreenshots":{"type":"array","items":{"type":"string","format":"uri"}},"appletvScreenshots":{"type":"array","items":{"type":"string","format":"uri"}},"supportedDevices":{"type":"array","items":{"type":"string"}}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/appstore/search":{"get":{"operationId":"searchApps","summary":"Search applications by term","description":"Search the App Store for applications matching `term`. Use `page` for pagination or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.0002 per item\n\n**Minimum charge:** $0.002 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["appstore"],"parameters":[{"name":"term","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":255}},{"name":"country","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":2}},{"name":"lang","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2,3}(-[a-z]{2,3})?$"}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":5}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Application"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get similar applications

> Returns applications similar to the supplied one. Provide exactly one of \`appId\` or \`bundleId\`.\
> \
> \*\*Price:\*\* $0.0002 per item\
> \
> \*\*Minimum charge:\*\* $0.002 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"appstore","description":"Apple App Store: app lookup, search, similar apps, reviews, developer apps."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"Application":{"type":"object","properties":{"id":{"type":"integer"},"appId":{"type":"string"},"title":{"type":"string"},"url":{"type":"string","format":"uri"},"description":{"type":"string"},"icon":{"type":"string","format":"uri"},"genres":{"type":"array","items":{"type":"string"}},"genreIds":{"type":"array","items":{"type":"string"}},"primaryGenre":{"type":"string"},"primaryGenreId":{"type":"integer"},"contentRating":{"type":"string"},"languages":{"type":"array","items":{"type":"string"}},"size":{"type":"string"},"requiredOsVersion":{"type":"string"},"released":{"type":"string","format":"date-time"},"updated":{"type":"string","format":"date-time"},"releaseNotes":{"type":"string"},"version":{"type":"string"},"price":{"type":"number"},"currency":{"type":"string"},"free":{"type":"boolean"},"developerId":{"type":"integer"},"developer":{"type":"string"},"developerUrl":{"type":"string","format":"uri"},"developerWebsite":{"type":"string","format":"uri"},"score":{"type":"number"},"reviews":{"type":"integer"},"currentVersionScore":{"type":"number"},"currentVersionReviews":{"type":"integer"},"screenshots":{"type":"array","items":{"type":"string","format":"uri"}},"ipadScreenshots":{"type":"array","items":{"type":"string","format":"uri"}},"appletvScreenshots":{"type":"array","items":{"type":"string","format":"uri"}},"supportedDevices":{"type":"array","items":{"type":"string"}}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/appstore/similar":{"get":{"operationId":"getSimilarApps","summary":"Get similar applications","description":"Returns applications similar to the supplied one. Provide exactly one of `appId` or `bundleId`.\n\n**Price:** $0.0002 per item\n\n**Minimum charge:** $0.002 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["appstore"],"parameters":[{"name":"appId","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{8,12}$"}},{"name":"bundleId","in":"query","required":false,"schema":{"type":"string","maxLength":255,"pattern":"^[A-Za-z0-9.]+$"}},{"name":"country","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":2}},{"name":"lang","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2,3}(-[a-z]{2,3})?$"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Application"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get reviews for an application

> Returns reviews for one application. Provide exactly one of \`appId\` or \`bundleId\`. Use \`page\` for pagination or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.0002 per item\
> \
> \*\*Minimum charge:\*\* $0.002 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"appstore","description":"Apple App Store: app lookup, search, similar apps, reviews, developer apps."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"Review":{"type":"object","properties":{"id":{"type":"string"},"date":{"type":"string","format":"date-time"},"userName":{"type":"string"},"userUrl":{"type":"string","format":"uri"},"version":{"type":"string"},"score":{"type":"integer","minimum":1,"maximum":5},"title":{"type":"string"},"text":{"type":"string"},"url":{"type":"string","format":"uri"},"country":{"type":"string"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/appstore/reviews":{"get":{"operationId":"getReviews","summary":"Get reviews for an application","description":"Returns reviews for one application. Provide exactly one of `appId` or `bundleId`. Use `page` for pagination or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.0002 per item\n\n**Minimum charge:** $0.002 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["appstore"],"parameters":[{"name":"appId","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{8,12}$"}},{"name":"bundleId","in":"query","required":false,"schema":{"type":"string","maxLength":255,"pattern":"^[A-Za-z0-9.]+$"}},{"name":"country","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":2}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["recent","helpful"]}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":10}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Review"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get all applications by a developer

> Returns the list of applications published by a single developer.\
> \
> \*\*Price:\*\* $0.001 per item\
> \
> \*\*Minimum charge:\*\* $0.001 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"appstore","description":"Apple App Store: app lookup, search, similar apps, reviews, developer apps."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"Application":{"type":"object","properties":{"id":{"type":"integer"},"appId":{"type":"string"},"title":{"type":"string"},"url":{"type":"string","format":"uri"},"description":{"type":"string"},"icon":{"type":"string","format":"uri"},"genres":{"type":"array","items":{"type":"string"}},"genreIds":{"type":"array","items":{"type":"string"}},"primaryGenre":{"type":"string"},"primaryGenreId":{"type":"integer"},"contentRating":{"type":"string"},"languages":{"type":"array","items":{"type":"string"}},"size":{"type":"string"},"requiredOsVersion":{"type":"string"},"released":{"type":"string","format":"date-time"},"updated":{"type":"string","format":"date-time"},"releaseNotes":{"type":"string"},"version":{"type":"string"},"price":{"type":"number"},"currency":{"type":"string"},"free":{"type":"boolean"},"developerId":{"type":"integer"},"developer":{"type":"string"},"developerUrl":{"type":"string","format":"uri"},"developerWebsite":{"type":"string","format":"uri"},"score":{"type":"number"},"reviews":{"type":"integer"},"currentVersionScore":{"type":"number"},"currentVersionReviews":{"type":"integer"},"screenshots":{"type":"array","items":{"type":"string","format":"uri"}},"ipadScreenshots":{"type":"array","items":{"type":"string","format":"uri"}},"appletvScreenshots":{"type":"array","items":{"type":"string","format":"uri"}},"supportedDevices":{"type":"array","items":{"type":"string"}}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/appstore/developer":{"get":{"operationId":"getDeveloperApps","summary":"Get all applications by a developer","description":"Returns the list of applications published by a single developer.\n\n**Price:** $0.001 per item\n\n**Minimum charge:** $0.001 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["appstore"],"parameters":[{"name":"developerId","in":"query","required":true,"schema":{"type":"string","pattern":"^\\d{8,12}$"}},{"name":"country","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":2}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Application"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```


# Duckduckgo

DuckDuckGo: web search, images, news, videos.

## Search the web via DuckDuckGo

> Returns web search results for \`term\`.\
> \
> \*\*Price:\*\* $0.0005 per item\
> \
> \*\*Minimum charge:\*\* $0.005 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"duckduckgo","description":"DuckDuckGo: web search, images, news, videos."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"DuckDuckGoSearchResult":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"hostname":{"type":"string"},"icon":{"type":"string","format":"uri"},"url":{"type":"string","format":"uri"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/duckduckgo/search":{"get":{"operationId":"duckduckgoSearch","summary":"Search the web via DuckDuckGo","description":"Returns web search results for `term`.\n\n**Price:** $0.0005 per item\n\n**Minimum charge:** $0.005 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["duckduckgo"],"parameters":[{"name":"term","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":255}},{"name":"region","in":"query","required":true,"schema":{"type":"string","enum":["wt-wt","ar-es","au-en","at-de","be-fr","be-nl","br-pt","bg-bg","ca-en","ca-fr","ct-ca","cl-es","cn-zh","co-es","hr-hr","cz-cs","dk-da","ee-et","fi-fi","fr-fr","de-de","gr-el","hk-tzh","hu-hu","in-en","id-en","ie-en","il-en","it-it","jp-jp","kr-kr","lv-lv","lt-lt","my-en","mx-es","nl-nl","nz-en","no-no","pk-en","pe-es","ph-en","pl-pl","pt-pt","ro-ro","ru-ru","xa-ar","sg-en","sk-sk","sl-sl","za-en","es-ca","es-es","se-sv","ch-de","ch-fr","tw-tzh","th-en","tr-tr","us-en","us-es","ua-uk","uk-en","vn-en"]}},{"name":"safeSearch","in":"query","required":true,"schema":{"type":"string","enum":["off","moderate","strict"]}},{"name":"offset","in":"query","required":false,"schema":{"type":"number","minimum":0}},{"name":"time","in":"query","required":false,"schema":{"type":"string","enum":["a","d","w","m","y"]}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/DuckDuckGoSearchResult"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Search images via DuckDuckGo

> Returns image search results for \`term\`.\
> \
> \*\*Price:\*\* $0.0005 per item\
> \
> \*\*Minimum charge:\*\* $0.005 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"duckduckgo","description":"DuckDuckGo: web search, images, news, videos."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"DuckDuckGoImage":{"type":"object","properties":{"title":{"type":"string"},"image":{"type":"string","format":"uri"},"thumbnail":{"type":"string","format":"uri"},"image_token":{"type":"string"},"thumbnail_token":{"type":"string"},"width":{"type":"integer"},"height":{"type":"integer"},"source":{"type":"string"},"url":{"type":"string","format":"uri"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/duckduckgo/images":{"get":{"operationId":"duckduckgoImages","summary":"Search images via DuckDuckGo","description":"Returns image search results for `term`.\n\n**Price:** $0.0005 per item\n\n**Minimum charge:** $0.005 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["duckduckgo"],"parameters":[{"name":"term","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":255}},{"name":"region","in":"query","required":true,"schema":{"type":"string","enum":["wt-wt","ar-es","au-en","at-de","be-fr","be-nl","br-pt","bg-bg","ca-en","ca-fr","ct-ca","cl-es","cn-zh","co-es","hr-hr","cz-cs","dk-da","ee-et","fi-fi","fr-fr","de-de","gr-el","hk-tzh","hu-hu","in-en","id-en","ie-en","il-en","it-it","jp-jp","kr-kr","lv-lv","lt-lt","my-en","mx-es","nl-nl","nz-en","no-no","pk-en","pe-es","ph-en","pl-pl","pt-pt","ro-ro","ru-ru","xa-ar","sg-en","sk-sk","sl-sl","za-en","es-ca","es-es","se-sv","ch-de","ch-fr","tw-tzh","th-en","tr-tr","us-en","us-es","ua-uk","uk-en","vn-en"]}},{"name":"safeSearch","in":"query","required":true,"schema":{"type":"string","enum":["off","moderate","strict"]}},{"name":"offset","in":"query","required":false,"schema":{"type":"number","minimum":0}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/DuckDuckGoImage"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Search news articles via DuckDuckGo

> Returns news articles for \`term\`. Use \`time\` to limit recency (\`a\`=any, \`d\`=day, \`w\`=week, \`m\`=month, \`y\`=year).\
> \
> \*\*Price:\*\* $0.0005 per item\
> \
> \*\*Minimum charge:\*\* $0.005 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"duckduckgo","description":"DuckDuckGo: web search, images, news, videos."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"DuckDuckGoNewsArticle":{"type":"object","properties":{"title":{"type":"string"},"excerpt":{"type":"string"},"url":{"type":"string","format":"uri"},"image":{"type":"string","format":"uri"},"date":{"type":"integer","description":"Unix timestamp in seconds"},"relativeTime":{"type":"string"},"syndicate":{"type":"string"},"isOld":{"type":"boolean"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/duckduckgo/news":{"get":{"operationId":"duckduckgoNews","summary":"Search news articles via DuckDuckGo","description":"Returns news articles for `term`. Use `time` to limit recency (`a`=any, `d`=day, `w`=week, `m`=month, `y`=year).\n\n**Price:** $0.0005 per item\n\n**Minimum charge:** $0.005 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["duckduckgo"],"parameters":[{"name":"term","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":255}},{"name":"region","in":"query","required":true,"schema":{"type":"string","enum":["wt-wt","ar-es","au-en","at-de","be-fr","be-nl","br-pt","bg-bg","ca-en","ca-fr","ct-ca","cl-es","cn-zh","co-es","hr-hr","cz-cs","dk-da","ee-et","fi-fi","fr-fr","de-de","gr-el","hk-tzh","hu-hu","in-en","id-en","ie-en","il-en","it-it","jp-jp","kr-kr","lv-lv","lt-lt","my-en","mx-es","nl-nl","nz-en","no-no","pk-en","pe-es","ph-en","pl-pl","pt-pt","ro-ro","ru-ru","xa-ar","sg-en","sk-sk","sl-sl","za-en","es-ca","es-es","se-sv","ch-de","ch-fr","tw-tzh","th-en","tr-tr","us-en","us-es","ua-uk","uk-en","vn-en"]}},{"name":"safeSearch","in":"query","required":true,"schema":{"type":"string","enum":["off","moderate","strict"]}},{"name":"offset","in":"query","required":false,"schema":{"type":"number","minimum":0}},{"name":"time","in":"query","required":false,"schema":{"type":"string","enum":["a","d","w","m","y"]}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/DuckDuckGoNewsArticle"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Search videos via DuckDuckGo

> Returns video search results for \`term\`.\
> \
> \*\*Price:\*\* $0.0005 per item\
> \
> \*\*Minimum charge:\*\* $0.005 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"duckduckgo","description":"DuckDuckGo: web search, images, news, videos."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"DuckDuckGoVideo":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"url":{"type":"string","format":"uri"},"image":{"type":"string","format":"uri"},"duration":{"type":"string"},"publishedOn":{"type":"string"},"published":{"type":"string","format":"date-time"},"publisher":{"type":"string"},"viewCount":{"type":"integer"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/duckduckgo/videos":{"get":{"operationId":"duckduckgoVideos","summary":"Search videos via DuckDuckGo","description":"Returns video search results for `term`.\n\n**Price:** $0.0005 per item\n\n**Minimum charge:** $0.005 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["duckduckgo"],"parameters":[{"name":"term","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":255}},{"name":"region","in":"query","required":true,"schema":{"type":"string","enum":["wt-wt","ar-es","au-en","at-de","be-fr","be-nl","br-pt","bg-bg","ca-en","ca-fr","ct-ca","cl-es","cn-zh","co-es","hr-hr","cz-cs","dk-da","ee-et","fi-fi","fr-fr","de-de","gr-el","hk-tzh","hu-hu","in-en","id-en","ie-en","il-en","it-it","jp-jp","kr-kr","lv-lv","lt-lt","my-en","mx-es","nl-nl","nz-en","no-no","pk-en","pe-es","ph-en","pl-pl","pt-pt","ro-ro","ru-ru","xa-ar","sg-en","sk-sk","sl-sl","za-en","es-ca","es-es","se-sv","ch-de","ch-fr","tw-tzh","th-en","tr-tr","us-en","us-es","ua-uk","uk-en","vn-en"]}},{"name":"safeSearch","in":"query","required":true,"schema":{"type":"string","enum":["off","moderate","strict"]}},{"name":"offset","in":"query","required":false,"schema":{"type":"number","minimum":0}},{"name":"time","in":"query","required":false,"schema":{"type":"string","enum":["a","d","w","m","y"]}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/DuckDuckGoVideo"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```


# Google Maps

Google Maps: place search, place details, and place reviews.

## Search Google Maps places

> Search Google Maps for places by free-text \`query\`. Use \`offset\` (a numeric string) to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive). Optional geo filters: \`latitude\`, \`longitude\`, \`zoom\`.\
> \
> \*\*Price:\*\* $0.001 per item\
> \
> \*\*Minimum charge:\*\* $0.01 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"google-maps","description":"Google Maps: place search, place details, and place reviews."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"GoogleMapsPlaceSearchResult":{"type":"object","description":"Place result returned by /google-maps/place/search.","properties":{"url":{"type":"string","format":"uri"},"title":{"type":"string"},"description":{"type":"string","nullable":true},"address":{"type":"string"},"neighborhood":{"type":"string","nullable":true},"street":{"type":"string","nullable":true},"district":{"type":"string","nullable":true},"postalCode":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"country":{"type":"string","nullable":true},"location":{"$ref":"#/components/schemas/GoogleMapsLocation"},"website":{"type":"string","format":"uri","nullable":true},"phone":{"type":"string","nullable":true},"price":{"type":"string","nullable":true},"totalScore":{"type":"number","format":"float"},"placeId":{"type":"string"},"fid":{"type":"string","description":"Google Maps feature ID, of the form '0x...:0x...'."},"reviewsCount":{"type":"integer"},"categories":{"type":"array","items":{"type":"string"}},"openingHours":{"type":"array","description":"Opening hours per weekday.","items":{"type":"object","additionalProperties":true}},"additionalInfo":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"type":"boolean"}},"description":"Grouped feature flags (e.g. Service options, Accessibility, Offerings)."}}},"GoogleMapsLocation":{"type":"object","description":"Latitude/longitude pair.","properties":{"lat":{"type":"number","format":"float"},"lng":{"type":"number","format":"float"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/google-maps/place/search":{"get":{"operationId":"searchGoogleMapsPlaces","summary":"Search Google Maps places","description":"Search Google Maps for places by free-text `query`. Use `offset` (a numeric string) to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive). Optional geo filters: `latitude`, `longitude`, `zoom`.\n\n**Price:** $0.001 per item\n\n**Minimum charge:** $0.01 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["google-maps"],"parameters":[{"name":"query","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":200}},{"name":"countryCode","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":5}},{"name":"languageCode","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":10}},{"name":"latitude","in":"query","required":false,"schema":{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$"}},{"name":"longitude","in":"query","required":false,"schema":{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$"}},{"name":"zoom","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d+(\\.\\d+)?$"}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"offset","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d+$"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/GoogleMapsPlaceSearchResult"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get a Google Maps place by FID

> Returns full details for a Google Maps place identified by its FID (e.g. \`0x47e671d877937b0f:0xb975fcfa192f84d4\`).\
> \
> \*\*Price:\*\* $0.005 per item\
> \
> \*\*Minimum charge:\*\* $0.005 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"google-maps","description":"Google Maps: place search, place details, and place reviews."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"GoogleMapsPlace":{"type":"object","description":"Full place detail returned by /google-maps/place/{fid}.","properties":{"url":{"type":"string","format":"uri"},"title":{"type":"string"},"address":{"type":"string"},"neighborhood":{"type":"string","nullable":true},"street":{"type":"string","nullable":true},"district":{"type":"string","nullable":true},"postalCode":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"location":{"$ref":"#/components/schemas/GoogleMapsLocation"},"website":{"type":"string","format":"uri","nullable":true},"phone":{"type":"string","nullable":true},"phoneUnformatted":{"type":"string","nullable":true},"plusCode":{"type":"string","nullable":true},"price":{"type":"string","nullable":true},"totalScore":{"type":"number","format":"float"},"placeId":{"type":"string"},"cid":{"type":"string"},"fid":{"type":"string"},"reviewsCount":{"type":"integer"},"reviewsDistribution":{"$ref":"#/components/schemas/GoogleMapsReviewsDistribution"},"categories":{"type":"array","items":{"type":"string"}},"additionalInfo":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"type":"boolean"}},"description":"Grouped feature flags (e.g. Service options, Accessibility, Amenities)."}}},"GoogleMapsLocation":{"type":"object","description":"Latitude/longitude pair.","properties":{"lat":{"type":"number","format":"float"},"lng":{"type":"number","format":"float"}}},"GoogleMapsReviewsDistribution":{"type":"object","properties":{"oneStar":{"type":"integer"},"twoStar":{"type":"integer"},"threeStar":{"type":"integer"},"fourStar":{"type":"integer"},"fiveStar":{"type":"integer"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/google-maps/place/{fid}":{"get":{"operationId":"getGoogleMapsPlace","summary":"Get a Google Maps place by FID","description":"Returns full details for a Google Maps place identified by its FID (e.g. `0x47e671d877937b0f:0xb975fcfa192f84d4`).\n\n**Price:** $0.005 per item\n\n**Minimum charge:** $0.005 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["google-maps"],"parameters":[{"name":"fid","in":"path","required":true,"schema":{"type":"string","pattern":"^0x[a-fA-F0-9]+:0x[a-fA-F0-9]+$"}},{"name":"countryCode","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":5}},{"name":"languageCode","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":10}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/GoogleMapsPlace"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get reviews for a Google Maps place

> Returns reviews for a place identified by its full Google Maps URL. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.001 per item\
> \
> \*\*Minimum charge:\*\* $0.01 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"google-maps","description":"Google Maps: place search, place details, and place reviews."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"GoogleMapsPlaceReview":{"type":"object","properties":{"id":{"type":"string"},"link":{"type":"string","format":"uri"},"text":{"type":"string"},"stars":{"type":"integer","minimum":1,"maximum":5},"date":{"type":"integer","description":"Unix timestamp in milliseconds"},"dateIso":{"type":"string","format":"date-time"},"likes":{"type":"integer","nullable":true},"photos":{"type":"array","items":{"type":"string"}},"reviewer":{"$ref":"#/components/schemas/GoogleMapsReviewer"},"ownersResponse":{"type":"object","additionalProperties":true,"description":"Owner's response to the review (empty object when none)."},"details":{"type":"object","additionalProperties":{"type":"string"},"description":"Free-form key/value pairs of review metadata (e.g. \"Visited on\", \"Wait time\")."}}},"GoogleMapsReviewer":{"type":"object","properties":{"name":{"type":"string"},"photo":{"type":"string","format":"uri"},"url":{"type":"string","format":"uri"},"totalReviews":{"type":"integer"},"totalPhotos":{"type":"integer"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/google-maps/place/reviews":{"get":{"operationId":"getPlaceReviews","summary":"Get reviews for a Google Maps place","description":"Returns reviews for a place identified by its full Google Maps URL. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.001 per item\n\n**Minimum charge:** $0.01 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["google-maps"],"parameters":[{"name":"placeUrl","in":"query","required":true,"schema":{"type":"string","minLength":10,"maxLength":1000,"format":"uri"}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/GoogleMapsPlaceReview"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```


# Google News

Google News: list articles by category or search query.

## List Google News articles by category

> Returns top news articles for a given Google News category. \`languageCode\` selects locale (e.g. \`US:en\`, \`GB:en\`).\
> \
> \*\*Price:\*\* $0.0005 per item\
> \
> \*\*Minimum charge:\*\* $0.005 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"google-news","description":"Google News: list articles by category or search query."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"GoogleNewsArticle":{"type":"object","properties":{"title":{"type":"string"},"articleUrl":{"type":"string","format":"uri"},"publishedAt":{"type":"string","format":"date-time"},"imageUrl":{"type":"string","format":"uri-reference","nullable":true},"source":{"type":"string"},"sourceIconUrl":{"type":"string","format":"uri"},"author":{"type":"string","nullable":true}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/google-news/category":{"get":{"operationId":"getNewsByCategory","summary":"List Google News articles by category","description":"Returns top news articles for a given Google News category. `languageCode` selects locale (e.g. `US:en`, `GB:en`).\n\n**Price:** $0.0005 per item\n\n**Minimum charge:** $0.005 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["google-news"],"parameters":[{"name":"category","in":"query","required":true,"schema":{"type":"string","enum":["business","entertainment","health","latest","science","sport","technology","world"]}},{"name":"languageCode","in":"query","required":false,"schema":{"type":"string","enum":["US:en","AU:en","BW:en","CA:en","ET:en","GH:en","IN:en","ID:en","IE:en","IL:en","KE:en","LV:en","MY:en","NA:en","NZ:en","NG:en","PK:en","PH:en","SG:en","ZA:en","TZ:en","UG:en","GB:en","ZW:en","ID:id","CZ:cs","DE:de","AT:de","CH:de","AR:es-419","CL:es-419","CO:es-419","CU:es-419","ES:es","US:es-419","MX:es-419","PE:es-419","VE:es-419","BE:fr","CA:fr","FR:fr","MA:fr","SN:fr","CH:fr","IT:it","LV:lv","LT:lt","HU:hu","BE:nl","NL:nl","NO:no","PL:pl","BR:pt-419","PT:pt-150","RO:ro","SK:sk","SI:sl","SE:sv","VN:vi","TR:tr","GR:el","BG:bg","RU:ru","UA:ru","RS:sr","UA:uk","IL:he","AE:ar","SA:ar","LB:ar","EG:ar","IN:mr","IN:hi","BD:bn","IN:bn","IN:ta","IN:te","IN:ml","TH:th","CN:zh-Hans","TW:zh-Hant","HK:zh-Hant","JP:ja","KR:ko"]}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/GoogleNewsArticle"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Search Google News articles

> Returns articles matching \`query\`. \`resolveUrl=yes\` follows Google's redirect to the publisher URL.\
> \
> \*\*Price:\*\* $0.0005 per item\
> \
> \*\*Minimum charge:\*\* $0.005 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"google-news","description":"Google News: list articles by category or search query."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"GoogleNewsArticle":{"type":"object","properties":{"title":{"type":"string"},"articleUrl":{"type":"string","format":"uri"},"publishedAt":{"type":"string","format":"date-time"},"imageUrl":{"type":"string","format":"uri-reference","nullable":true},"source":{"type":"string"},"sourceIconUrl":{"type":"string","format":"uri"},"author":{"type":"string","nullable":true}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/google-news/search":{"get":{"operationId":"searchNews","summary":"Search Google News articles","description":"Returns articles matching `query`. `resolveUrl=yes` follows Google's redirect to the publisher URL.\n\n**Price:** $0.0005 per item\n\n**Minimum charge:** $0.005 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["google-news"],"parameters":[{"name":"query","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":500}},{"name":"languageCode","in":"query","required":false,"schema":{"type":"string","enum":["US:en","AU:en","BW:en","CA:en","ET:en","GH:en","IN:en","ID:en","IE:en","IL:en","KE:en","LV:en","MY:en","NA:en","NZ:en","NG:en","PK:en","PH:en","SG:en","ZA:en","TZ:en","UG:en","GB:en","ZW:en","ID:id","CZ:cs","DE:de","AT:de","CH:de","AR:es-419","CL:es-419","CO:es-419","CU:es-419","ES:es","US:es-419","MX:es-419","PE:es-419","VE:es-419","BE:fr","CA:fr","FR:fr","MA:fr","SN:fr","CH:fr","IT:it","LV:lv","LT:lt","HU:hu","BE:nl","NL:nl","NO:no","PL:pl","BR:pt-419","PT:pt-150","RO:ro","SK:sk","SI:sl","SE:sv","VN:vi","TR:tr","GR:el","BG:bg","RU:ru","UA:ru","RS:sr","UA:uk","IL:he","AE:ar","SA:ar","LB:ar","EG:ar","IN:mr","IN:hi","BD:bn","IN:bn","IN:ta","IN:te","IN:ml","TH:th","CN:zh-Hans","TW:zh-Hant","HK:zh-Hant","JP:ja","KR:ko"]}},{"name":"resolveUrl","in":"query","required":false,"schema":{"type":"string","enum":["yes","no"]}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/GoogleNewsArticle"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```


# Google Play

Google Play Store: app lookup, search, similar apps, reviews, permissions, data safety.

## Get a Google Play application by App ID

> Returns full details for a single app on the Google Play Store, identified by its package name (e.g. \`com.instagram.android\`).\
> \
> \*\*Price:\*\* $0.0002 per item\
> \
> \*\*Minimum charge:\*\* $0.002 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"google-play","description":"Google Play Store: app lookup, search, similar apps, reviews, permissions, data safety."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"GooglePlayApplication":{"type":"object","properties":{"title":{"type":"string"},"appId":{"type":"string"},"url":{"type":"string","format":"uri-reference"},"icon":{"type":"string","format":"uri"},"headerImage":{"type":"string","format":"uri"},"summary":{"type":"string"},"description":{"type":"string"},"descriptionHTML":{"type":"string"},"score":{"type":"number"},"scoreText":{"type":"string"},"ratings":{"type":"integer"},"reviews":{"type":"integer"},"histogram":{"type":"object","additionalProperties":{"type":"integer"},"description":"Rating histogram, keyed by star count (1-5)."},"free":{"type":"boolean"},"price":{"type":"number"},"priceText":{"type":"string"},"currency":{"type":"string"},"offersIAP":{"type":"boolean"},"adSupported":{"type":"boolean"},"installs":{"type":"string"},"minInstalls":{"type":"integer"},"maxInstalls":{"type":"integer"},"androidVersion":{"type":"string"},"androidVersionText":{"type":"string"},"updated":{"type":"integer","description":"Unix timestamp in milliseconds"},"genre":{"type":"string"},"genreId":{"type":"string"},"developer":{"type":"string"},"developerId":{"type":"string"},"developerEmail":{"type":"string"},"developerWebsite":{"type":"string","format":"uri"},"developerInternalID":{"type":"string"},"privacyPolicy":{"type":"string","format":"uri"},"playstoreUrl":{"type":"string","format":"uri"},"screenshots":{"type":"array","items":{"type":"string"}}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/google-play/apps":{"get":{"operationId":"getPlayApp","summary":"Get a Google Play application by App ID","description":"Returns full details for a single app on the Google Play Store, identified by its package name (e.g. `com.instagram.android`).\n\n**Price:** $0.0002 per item\n\n**Minimum charge:** $0.002 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["google-play"],"parameters":[{"name":"appId","in":"query","required":true,"schema":{"type":"string","maxLength":255,"pattern":"^[A-Za-z0-9._]+$"}},{"name":"country","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":2}},{"name":"lang","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2,3}(-[a-z]{2,3})?$"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/GooglePlayApplication"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Search Google Play applications

> Search the Google Play Store for apps matching \`search\`. Returns lightweight summaries.\
> \
> \*\*Price:\*\* $0.0002 per item\
> \
> \*\*Minimum charge:\*\* $0.002 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"google-play","description":"Google Play Store: app lookup, search, similar apps, reviews, permissions, data safety."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"GooglePlayApplicationSummary":{"type":"object","description":"Lightweight app reference returned by search and similar endpoints.","properties":{"title":{"type":"string"},"appId":{"type":"string"},"url":{"type":"string","format":"uri-reference"},"icon":{"type":"string","format":"uri"},"summary":{"type":"string"},"score":{"type":"number"},"scoreText":{"type":"string"},"free":{"type":"boolean"},"price":{"type":"number"},"currency":{"type":"string"},"playstoreUrl":{"type":"string","format":"uri"},"developer":{"type":"string"},"developerId":{"type":"string"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/google-play/search":{"get":{"operationId":"searchPlayApps","summary":"Search Google Play applications","description":"Search the Google Play Store for apps matching `search`. Returns lightweight summaries.\n\n**Price:** $0.0002 per item\n\n**Minimum charge:** $0.002 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["google-play"],"parameters":[{"name":"search","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":255}},{"name":"price","in":"query","required":false,"schema":{"type":"string","enum":["all","free","paid"]}},{"name":"country","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":2}},{"name":"lang","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2,3}(-[a-z]{2,3})?$"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/GooglePlayApplicationSummary"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get similar Google Play applications

> Returns apps Google Play surfaces as similar to the supplied \`appId\`.\
> \
> \*\*Price:\*\* $0.0002 per item\
> \
> \*\*Minimum charge:\*\* $0.002 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"google-play","description":"Google Play Store: app lookup, search, similar apps, reviews, permissions, data safety."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"GooglePlayApplicationSummary":{"type":"object","description":"Lightweight app reference returned by search and similar endpoints.","properties":{"title":{"type":"string"},"appId":{"type":"string"},"url":{"type":"string","format":"uri-reference"},"icon":{"type":"string","format":"uri"},"summary":{"type":"string"},"score":{"type":"number"},"scoreText":{"type":"string"},"free":{"type":"boolean"},"price":{"type":"number"},"currency":{"type":"string"},"playstoreUrl":{"type":"string","format":"uri"},"developer":{"type":"string"},"developerId":{"type":"string"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/google-play/similar":{"get":{"operationId":"getSimilarPlayApps","summary":"Get similar Google Play applications","description":"Returns apps Google Play surfaces as similar to the supplied `appId`.\n\n**Price:** $0.0002 per item\n\n**Minimum charge:** $0.002 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["google-play"],"parameters":[{"name":"appId","in":"query","required":true,"schema":{"type":"string","maxLength":255,"pattern":"^[A-Za-z0-9._]+$"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/GooglePlayApplicationSummary"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get reviews for a Google Play application

> Returns reviews for an app. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive). \`sort\` accepts \`NEWEST\`, \`RATING\`, or \`HELPFULNESS\`.\
> \
> \*\*Price:\*\* $0.0002 per item\
> \
> \*\*Minimum charge:\*\* $0.002 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"google-play","description":"Google Play Store: app lookup, search, similar apps, reviews, permissions, data safety."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"GooglePlayReview":{"type":"object","properties":{"id":{"type":"string"},"userName":{"type":"string"},"userImage":{"type":"string","format":"uri"},"date":{"type":"string","format":"date-time"},"score":{"type":"integer","minimum":1,"maximum":5},"scoreText":{"type":"string"},"url":{"type":"string","format":"uri"},"title":{"type":"string","nullable":true},"text":{"type":"string"},"replyDate":{"type":"string","format":"date-time","nullable":true},"replyText":{"type":"string","nullable":true},"version":{"type":"string"},"thumbsUp":{"type":"integer"},"country":{"type":"string"},"criterias":{"type":"array","items":{"$ref":"#/components/schemas/GooglePlayReviewCriteria"}}}},"GooglePlayReviewCriteria":{"type":"object","properties":{"criteria":{"type":"string"},"rating":{"type":"integer"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/google-play/reviews":{"get":{"operationId":"getPlayReviews","summary":"Get reviews for a Google Play application","description":"Returns reviews for an app. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive). `sort` accepts `NEWEST`, `RATING`, or `HELPFULNESS`.\n\n**Price:** $0.0002 per item\n\n**Minimum charge:** $0.002 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["google-play"],"parameters":[{"name":"appId","in":"query","required":true,"schema":{"type":"string","maxLength":255,"pattern":"^[A-Za-z0-9._]+$"}},{"name":"country","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":2}},{"name":"lang","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2,3}(-[a-z]{2,3})?$"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["NEWEST","RATING","HELPFULNESS"]}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/GooglePlayReview"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get permissions requested by a Google Play application

> Returns the list of Android permissions the app requests, grouped by category.\
> \
> \*\*Price:\*\* $0.0002 per item\
> \
> \*\*Minimum charge:\*\* $0.002 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"google-play","description":"Google Play Store: app lookup, search, similar apps, reviews, permissions, data safety."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"GooglePlayPermission":{"type":"object","properties":{"permission":{"type":"string"},"type":{"type":"string"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/google-play/permissions":{"get":{"operationId":"getPlayPermissions","summary":"Get permissions requested by a Google Play application","description":"Returns the list of Android permissions the app requests, grouped by category.\n\n**Price:** $0.0002 per item\n\n**Minimum charge:** $0.002 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["google-play"],"parameters":[{"name":"appId","in":"query","required":true,"schema":{"type":"string","maxLength":255,"pattern":"^[A-Za-z0-9._]+$"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/GooglePlayPermission"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get the Data Safety section for a Google Play application

> Returns the developer-declared Data Safety information: data shared, data collected, security practices, and privacy policy URL.\
> \
> \*\*Price:\*\* $0.0002 per item\
> \
> \*\*Minimum charge:\*\* $0.002 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"google-play","description":"Google Play Store: app lookup, search, similar apps, reviews, permissions, data safety."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"GooglePlayDataSafety":{"type":"object","properties":{"sharedData":{"type":"array","items":{"$ref":"#/components/schemas/GooglePlayDataSafetyItem"}},"collectedData":{"type":"array","items":{"$ref":"#/components/schemas/GooglePlayDataSafetyItem"}},"securityPractices":{"type":"array","items":{"$ref":"#/components/schemas/GooglePlaySecurityPractice"}},"privacyPolicyUrl":{"type":"string","format":"uri"}}},"GooglePlayDataSafetyItem":{"type":"object","properties":{"data":{"type":"string"},"purpose":{"type":"string"},"type":{"type":"string"},"optional":{"type":"boolean"}}},"GooglePlaySecurityPractice":{"type":"object","properties":{"practice":{"type":"string"},"description":{"type":"string"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/google-play/data-safety":{"get":{"operationId":"getPlayDataSafety","summary":"Get the Data Safety section for a Google Play application","description":"Returns the developer-declared Data Safety information: data shared, data collected, security practices, and privacy policy URL.\n\n**Price:** $0.0002 per item\n\n**Minimum charge:** $0.002 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["google-play"],"parameters":[{"name":"appId","in":"query","required":true,"schema":{"type":"string","maxLength":255,"pattern":"^[A-Za-z0-9._]+$"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/GooglePlayDataSafety"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```


# Google Search

Google Search: web search results.

## Search the web via Google

> Returns Google search results for \`query\`. Use \`page\` for pagination or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.005 per item\
> \
> \*\*Minimum charge:\*\* $0.05 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"google-search","description":"Google Search: web search results."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"GoogleSearchResult":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"link":{"type":"string","format":"uri"},"snippet":{"type":"string"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/google-search/search":{"get":{"operationId":"googleSearch","summary":"Search the web via Google","description":"Returns Google search results for `query`. Use `page` for pagination or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.005 per item\n\n**Minimum charge:** $0.05 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["google-search"],"parameters":[{"name":"query","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":1000}},{"name":"safe","in":"query","required":false,"schema":{"type":"string"}},{"name":"hl","in":"query","required":false,"schema":{"type":"string"}},{"name":"country","in":"query","required":false,"schema":{"type":"string"}},{"name":"count","in":"query","required":false,"schema":{"type":"string","enum":["10"]}},{"name":"noEncode","in":"query","required":false,"schema":{"type":"string"}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":9007199254740991}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/GoogleSearchResult"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```


# Reddit

Reddit: posts, comments, users, and subreddits.

## Search Reddit posts

> Search Reddit posts by keyword. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive). \`sort\` is \`relevance\` (default), \`hot\`, \`top\`, \`new\`, or \`comments\`.\
> \
> \*\*Price:\*\* $0.001 per item\
> \
> \*\*Minimum charge:\*\* $0.01 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"reddit","description":"Reddit: posts, comments, users, and subreddits."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"RedditPost":{"type":"object","description":"A Reddit post (link or self-text).","properties":{"type":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"text":{"type":"string"},"textHTML":{"type":"string"},"url":{"type":"string"},"created":{"type":"integer","description":"Unix timestamp in seconds."},"editedAt":{"oneOf":[{"type":"integer"},{"type":"boolean"}]},"score":{"type":"integer"},"upvotes":{"type":"integer"},"downvotes":{"type":"integer"},"upvoteRatio":{"type":"number"},"numComments":{"type":"integer"},"viewCount":{"type":"integer","nullable":true},"goldCount":{"type":"integer"},"numCrossposts":{"type":"integer"},"isLocked":{"type":"boolean"},"isStickied":{"type":"boolean"},"isArchived":{"type":"boolean"},"isOriginalContent":{"type":"boolean"},"isCrosspostable":{"type":"boolean"},"isNSFW":{"type":"boolean"},"isSpoiler":{"type":"boolean"},"isMediaOnly":{"type":"boolean"},"isSponsored":{"type":"boolean"},"isMeta":{"type":"boolean"},"isGildable":{"type":"boolean"},"isScoreHidden":{"type":"boolean"},"contestMode":{"type":"boolean"},"hidden":{"type":"boolean"},"sendReplies":{"type":"boolean"},"author":{"$ref":"#/components/schemas/RedditAuthor"},"subreddit":{"$ref":"#/components/schemas/RedditSubredditRef"},"flair":{"type":"object","nullable":true},"thumbnail":{"type":"object","additionalProperties":true},"preview":{"type":"object","additionalProperties":true},"awards":{"type":"array","items":{"type":"object","additionalProperties":true}},"flairs":{"type":"array","items":{"type":"object","additionalProperties":true}},"gallery":{"type":"array","items":{"type":"object","additionalProperties":true}},"crosspostParents":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"RedditAuthor":{"type":"object","description":"Lightweight author reference embedded in posts and comments.","properties":{"id":{"type":"string"},"username":{"type":"string"},"isBlocked":{"type":"boolean"},"isDeleted":{"type":"boolean"}}},"RedditSubredditRef":{"type":"object","description":"Lightweight subreddit reference embedded in posts and comments.","properties":{"id":{"type":"string"},"name":{"type":"string"},"prefixedName":{"type":"string"},"type":{"type":"string"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/reddit/search/post":{"get":{"operationId":"searchRedditPosts","summary":"Search Reddit posts","description":"Search Reddit posts by keyword. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive). `sort` is `relevance` (default), `hot`, `top`, `new`, or `comments`.\n\n**Price:** $0.001 per item\n\n**Minimum charge:** $0.01 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["reddit"],"parameters":[{"name":"keyword","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["relevance","hot","top","new","comments"]}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/RedditPost"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get a Reddit post by ID

> Returns a single Reddit post by its ID.\
> \
> \*\*Price:\*\* $0.005 per item\
> \
> \*\*Minimum charge:\*\* $0.005 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"reddit","description":"Reddit: posts, comments, users, and subreddits."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"RedditPost":{"type":"object","description":"A Reddit post (link or self-text).","properties":{"type":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"text":{"type":"string"},"textHTML":{"type":"string"},"url":{"type":"string"},"created":{"type":"integer","description":"Unix timestamp in seconds."},"editedAt":{"oneOf":[{"type":"integer"},{"type":"boolean"}]},"score":{"type":"integer"},"upvotes":{"type":"integer"},"downvotes":{"type":"integer"},"upvoteRatio":{"type":"number"},"numComments":{"type":"integer"},"viewCount":{"type":"integer","nullable":true},"goldCount":{"type":"integer"},"numCrossposts":{"type":"integer"},"isLocked":{"type":"boolean"},"isStickied":{"type":"boolean"},"isArchived":{"type":"boolean"},"isOriginalContent":{"type":"boolean"},"isCrosspostable":{"type":"boolean"},"isNSFW":{"type":"boolean"},"isSpoiler":{"type":"boolean"},"isMediaOnly":{"type":"boolean"},"isSponsored":{"type":"boolean"},"isMeta":{"type":"boolean"},"isGildable":{"type":"boolean"},"isScoreHidden":{"type":"boolean"},"contestMode":{"type":"boolean"},"hidden":{"type":"boolean"},"sendReplies":{"type":"boolean"},"author":{"$ref":"#/components/schemas/RedditAuthor"},"subreddit":{"$ref":"#/components/schemas/RedditSubredditRef"},"flair":{"type":"object","nullable":true},"thumbnail":{"type":"object","additionalProperties":true},"preview":{"type":"object","additionalProperties":true},"awards":{"type":"array","items":{"type":"object","additionalProperties":true}},"flairs":{"type":"array","items":{"type":"object","additionalProperties":true}},"gallery":{"type":"array","items":{"type":"object","additionalProperties":true}},"crosspostParents":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"RedditAuthor":{"type":"object","description":"Lightweight author reference embedded in posts and comments.","properties":{"id":{"type":"string"},"username":{"type":"string"},"isBlocked":{"type":"boolean"},"isDeleted":{"type":"boolean"}}},"RedditSubredditRef":{"type":"object","description":"Lightweight subreddit reference embedded in posts and comments.","properties":{"id":{"type":"string"},"name":{"type":"string"},"prefixedName":{"type":"string"},"type":{"type":"string"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/reddit/post/{id}":{"get":{"operationId":"getRedditPost","summary":"Get a Reddit post by ID","description":"Returns a single Reddit post by its ID.\n\n**Price:** $0.005 per item\n\n**Minimum charge:** $0.005 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["reddit"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":32}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/RedditPost"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get comments on a Reddit post

> Returns the top-level comment tree for a Reddit post.\
> \
> \*\*Price:\*\* $0.001 per item\
> \
> \*\*Minimum charge:\*\* $0.01 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"reddit","description":"Reddit: posts, comments, users, and subreddits."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"RedditComment":{"type":"object","description":"A Reddit comment.","properties":{"type":{"type":"string"},"id":{"type":"string"},"postId":{"type":"string"},"parentId":{"type":"string"},"body":{"type":"string"},"bodyHTML":{"type":"string"},"url":{"type":"string"},"createdAt":{"type":"integer","description":"Unix timestamp in seconds."},"editedAt":{"oneOf":[{"type":"integer"},{"type":"boolean"}]},"score":{"type":"integer"},"upvotes":{"type":"integer"},"downvotes":{"type":"integer"},"goldCount":{"type":"integer"},"totalAwardsReceived":{"type":"integer"},"controversiality":{"type":"integer"},"isLocked":{"type":"boolean"},"isStickied":{"type":"boolean"},"isArchived":{"type":"boolean"},"isSaved":{"type":"boolean"},"isScoreHidden":{"type":"boolean"},"isCollapsed":{"type":"boolean"},"noFollow":{"type":"boolean"},"sendReplies":{"type":"boolean"},"hasReplies":{"type":"boolean"},"author":{"$ref":"#/components/schemas/RedditAuthor"},"subreddit":{"$ref":"#/components/schemas/RedditSubredditRef"},"awards":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"RedditAuthor":{"type":"object","description":"Lightweight author reference embedded in posts and comments.","properties":{"id":{"type":"string"},"username":{"type":"string"},"isBlocked":{"type":"boolean"},"isDeleted":{"type":"boolean"}}},"RedditSubredditRef":{"type":"object","description":"Lightweight subreddit reference embedded in posts and comments.","properties":{"id":{"type":"string"},"name":{"type":"string"},"prefixedName":{"type":"string"},"type":{"type":"string"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/reddit/post/{id}/comments":{"get":{"operationId":"getRedditPostComments","summary":"Get comments on a Reddit post","description":"Returns the top-level comment tree for a Reddit post.\n\n**Price:** $0.001 per item\n\n**Minimum charge:** $0.01 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["reddit"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":32}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["best","top","new","controversial","old","qa"]}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/RedditComment"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get replies to a Reddit comment

> Returns replies to a specific comment on a Reddit post.\
> \
> \*\*Price:\*\* $0.001 per item\
> \
> \*\*Minimum charge:\*\* $0.01 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"reddit","description":"Reddit: posts, comments, users, and subreddits."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"RedditComment":{"type":"object","description":"A Reddit comment.","properties":{"type":{"type":"string"},"id":{"type":"string"},"postId":{"type":"string"},"parentId":{"type":"string"},"body":{"type":"string"},"bodyHTML":{"type":"string"},"url":{"type":"string"},"createdAt":{"type":"integer","description":"Unix timestamp in seconds."},"editedAt":{"oneOf":[{"type":"integer"},{"type":"boolean"}]},"score":{"type":"integer"},"upvotes":{"type":"integer"},"downvotes":{"type":"integer"},"goldCount":{"type":"integer"},"totalAwardsReceived":{"type":"integer"},"controversiality":{"type":"integer"},"isLocked":{"type":"boolean"},"isStickied":{"type":"boolean"},"isArchived":{"type":"boolean"},"isSaved":{"type":"boolean"},"isScoreHidden":{"type":"boolean"},"isCollapsed":{"type":"boolean"},"noFollow":{"type":"boolean"},"sendReplies":{"type":"boolean"},"hasReplies":{"type":"boolean"},"author":{"$ref":"#/components/schemas/RedditAuthor"},"subreddit":{"$ref":"#/components/schemas/RedditSubredditRef"},"awards":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"RedditAuthor":{"type":"object","description":"Lightweight author reference embedded in posts and comments.","properties":{"id":{"type":"string"},"username":{"type":"string"},"isBlocked":{"type":"boolean"},"isDeleted":{"type":"boolean"}}},"RedditSubredditRef":{"type":"object","description":"Lightweight subreddit reference embedded in posts and comments.","properties":{"id":{"type":"string"},"name":{"type":"string"},"prefixedName":{"type":"string"},"type":{"type":"string"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/reddit/post/{id}/comments/{commentId}/replies":{"get":{"operationId":"getRedditCommentReplies","summary":"Get replies to a Reddit comment","description":"Returns replies to a specific comment on a Reddit post.\n\n**Price:** $0.001 per item\n\n**Minimum charge:** $0.01 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["reddit"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":32}},{"name":"commentId","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":32}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["best","top","new","controversial","old","qa"]}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/RedditComment"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get a Reddit user by username

> Returns a Reddit user profile by username (without \`u/\`).\
> \
> \*\*Price:\*\* $0.005 per item\
> \
> \*\*Minimum charge:\*\* $0.005 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"reddit","description":"Reddit: posts, comments, users, and subreddits."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"RedditUser":{"type":"object","description":"A Reddit user profile.","properties":{"type":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"createdAt":{"type":"integer","description":"Unix timestamp in seconds."},"iconImage":{"type":"string"},"snoovatarImage":{"type":"string"},"bannerImage":{"type":"string"},"awarderKarma":{"type":"integer"},"postKarma":{"type":"integer"},"commentKarma":{"type":"integer"},"totalKarma":{"type":"integer"},"hasVerifiedEmail":{"type":"boolean"},"over18":{"type":"boolean"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/reddit/user/{username}":{"get":{"operationId":"getRedditUser","summary":"Get a Reddit user by username","description":"Returns a Reddit user profile by username (without `u/`).\n\n**Price:** $0.005 per item\n\n**Minimum charge:** $0.005 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["reddit"],"parameters":[{"name":"username","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/RedditUser"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get posts submitted by a Reddit user

> Returns posts submitted by a Reddit user. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.001 per item\
> \
> \*\*Minimum charge:\*\* $0.01 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"reddit","description":"Reddit: posts, comments, users, and subreddits."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"RedditPost":{"type":"object","description":"A Reddit post (link or self-text).","properties":{"type":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"text":{"type":"string"},"textHTML":{"type":"string"},"url":{"type":"string"},"created":{"type":"integer","description":"Unix timestamp in seconds."},"editedAt":{"oneOf":[{"type":"integer"},{"type":"boolean"}]},"score":{"type":"integer"},"upvotes":{"type":"integer"},"downvotes":{"type":"integer"},"upvoteRatio":{"type":"number"},"numComments":{"type":"integer"},"viewCount":{"type":"integer","nullable":true},"goldCount":{"type":"integer"},"numCrossposts":{"type":"integer"},"isLocked":{"type":"boolean"},"isStickied":{"type":"boolean"},"isArchived":{"type":"boolean"},"isOriginalContent":{"type":"boolean"},"isCrosspostable":{"type":"boolean"},"isNSFW":{"type":"boolean"},"isSpoiler":{"type":"boolean"},"isMediaOnly":{"type":"boolean"},"isSponsored":{"type":"boolean"},"isMeta":{"type":"boolean"},"isGildable":{"type":"boolean"},"isScoreHidden":{"type":"boolean"},"contestMode":{"type":"boolean"},"hidden":{"type":"boolean"},"sendReplies":{"type":"boolean"},"author":{"$ref":"#/components/schemas/RedditAuthor"},"subreddit":{"$ref":"#/components/schemas/RedditSubredditRef"},"flair":{"type":"object","nullable":true},"thumbnail":{"type":"object","additionalProperties":true},"preview":{"type":"object","additionalProperties":true},"awards":{"type":"array","items":{"type":"object","additionalProperties":true}},"flairs":{"type":"array","items":{"type":"object","additionalProperties":true}},"gallery":{"type":"array","items":{"type":"object","additionalProperties":true}},"crosspostParents":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"RedditAuthor":{"type":"object","description":"Lightweight author reference embedded in posts and comments.","properties":{"id":{"type":"string"},"username":{"type":"string"},"isBlocked":{"type":"boolean"},"isDeleted":{"type":"boolean"}}},"RedditSubredditRef":{"type":"object","description":"Lightweight subreddit reference embedded in posts and comments.","properties":{"id":{"type":"string"},"name":{"type":"string"},"prefixedName":{"type":"string"},"type":{"type":"string"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/reddit/user/{username}/posts":{"get":{"operationId":"getRedditUserPosts","summary":"Get posts submitted by a Reddit user","description":"Returns posts submitted by a Reddit user. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.001 per item\n\n**Minimum charge:** $0.01 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["reddit"],"parameters":[{"name":"username","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["hot","top","new"]}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/RedditPost"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get comments authored by a Reddit user

> Returns comments authored by a Reddit user. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.001 per item\
> \
> \*\*Minimum charge:\*\* $0.01 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"reddit","description":"Reddit: posts, comments, users, and subreddits."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"RedditComment":{"type":"object","description":"A Reddit comment.","properties":{"type":{"type":"string"},"id":{"type":"string"},"postId":{"type":"string"},"parentId":{"type":"string"},"body":{"type":"string"},"bodyHTML":{"type":"string"},"url":{"type":"string"},"createdAt":{"type":"integer","description":"Unix timestamp in seconds."},"editedAt":{"oneOf":[{"type":"integer"},{"type":"boolean"}]},"score":{"type":"integer"},"upvotes":{"type":"integer"},"downvotes":{"type":"integer"},"goldCount":{"type":"integer"},"totalAwardsReceived":{"type":"integer"},"controversiality":{"type":"integer"},"isLocked":{"type":"boolean"},"isStickied":{"type":"boolean"},"isArchived":{"type":"boolean"},"isSaved":{"type":"boolean"},"isScoreHidden":{"type":"boolean"},"isCollapsed":{"type":"boolean"},"noFollow":{"type":"boolean"},"sendReplies":{"type":"boolean"},"hasReplies":{"type":"boolean"},"author":{"$ref":"#/components/schemas/RedditAuthor"},"subreddit":{"$ref":"#/components/schemas/RedditSubredditRef"},"awards":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"RedditAuthor":{"type":"object","description":"Lightweight author reference embedded in posts and comments.","properties":{"id":{"type":"string"},"username":{"type":"string"},"isBlocked":{"type":"boolean"},"isDeleted":{"type":"boolean"}}},"RedditSubredditRef":{"type":"object","description":"Lightweight subreddit reference embedded in posts and comments.","properties":{"id":{"type":"string"},"name":{"type":"string"},"prefixedName":{"type":"string"},"type":{"type":"string"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/reddit/user/{username}/comments":{"get":{"operationId":"getRedditUserComments","summary":"Get comments authored by a Reddit user","description":"Returns comments authored by a Reddit user. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.001 per item\n\n**Minimum charge:** $0.01 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["reddit"],"parameters":[{"name":"username","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["hot","top","new"]}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/RedditComment"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get a subreddit profile

> Returns a subreddit profile by its name (without \`r/\`).\
> \
> \*\*Price:\*\* $0.005 per item\
> \
> \*\*Minimum charge:\*\* $0.005 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"reddit","description":"Reddit: posts, comments, users, and subreddits."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"RedditSubreddit":{"type":"object","description":"A subreddit profile.","properties":{"type":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"prefixedName":{"type":"string"},"title":{"type":"string"},"subType":{"type":"string"},"description":{"type":"string"},"descriptionHtml":{"type":"string"},"publicDescription":{"type":"string"},"publicDescriptionHtml":{"type":"string"},"submitText":{"type":"string"},"submitTextHtml":{"type":"string","nullable":true},"submissionType":{"type":"string"},"advertiserCategory":{"type":"string"},"language":{"type":"string"},"url":{"type":"string"},"headerTitle":{"type":"string"},"iconImage":{"type":"string"},"iconSize":{"type":"array","nullable":true,"items":{"type":"integer"}},"bannerImage":{"type":"string"},"bannerBackgroundImage":{"type":"string"},"bannerSize":{"type":"array","nullable":true,"items":{"type":"integer"}},"headerImage":{"type":"string"},"headerSize":{"type":"array","items":{"type":"integer"}},"primaryColor":{"type":"string"},"keyColor":{"type":"string"},"createdAt":{"type":"integer","description":"Unix timestamp in seconds."},"subscribers":{"type":"integer"},"commentScoreHideMins":{"type":"integer"},"notificationLevel":{"type":"string","nullable":true},"suggestedCommentSort":{"type":"string","nullable":true},"userFlairEnabled":{"type":"boolean"},"linkFlairEnabled":{"type":"boolean"},"wikiEnabled":{"type":"boolean"},"emojisEnabled":{"type":"boolean"},"spoilersEnabled":{"type":"boolean"},"allowImages":{"type":"boolean"},"allowVideos":{"type":"boolean"},"allowGalleries":{"type":"boolean"},"allowPolls":{"type":"boolean"},"allowPredictions":{"type":"boolean"},"allowTalks":{"type":"boolean"},"allowDiscovery":{"type":"boolean"},"isCrosspostable":{"type":"boolean"},"publicTraffic":{"type":"boolean"},"showMediaPreview":{"type":"boolean"},"showMedia":{"type":"boolean"},"collapseDeletedComments":{"type":"boolean"},"shouldArchivePosts":{"type":"boolean"},"canAssignUserFlair":{"type":"boolean"},"canAssignLinkFlair":{"type":"boolean"},"restrictCommenting":{"type":"boolean"},"restrictPosting":{"type":"boolean"},"communityReviewed":{"type":"boolean"},"flairPosition":{"type":"string"},"linkFlairPosition":{"type":"string"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/reddit/subreddit/{name}":{"get":{"operationId":"getRedditSubreddit","summary":"Get a subreddit profile","description":"Returns a subreddit profile by its name (without `r/`).\n\n**Price:** $0.005 per item\n\n**Minimum charge:** $0.005 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["reddit"],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/RedditSubreddit"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get posts from a subreddit

> Returns posts from a subreddit. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.001 per item\
> \
> \*\*Minimum charge:\*\* $0.01 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"reddit","description":"Reddit: posts, comments, users, and subreddits."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"RedditPost":{"type":"object","description":"A Reddit post (link or self-text).","properties":{"type":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"text":{"type":"string"},"textHTML":{"type":"string"},"url":{"type":"string"},"created":{"type":"integer","description":"Unix timestamp in seconds."},"editedAt":{"oneOf":[{"type":"integer"},{"type":"boolean"}]},"score":{"type":"integer"},"upvotes":{"type":"integer"},"downvotes":{"type":"integer"},"upvoteRatio":{"type":"number"},"numComments":{"type":"integer"},"viewCount":{"type":"integer","nullable":true},"goldCount":{"type":"integer"},"numCrossposts":{"type":"integer"},"isLocked":{"type":"boolean"},"isStickied":{"type":"boolean"},"isArchived":{"type":"boolean"},"isOriginalContent":{"type":"boolean"},"isCrosspostable":{"type":"boolean"},"isNSFW":{"type":"boolean"},"isSpoiler":{"type":"boolean"},"isMediaOnly":{"type":"boolean"},"isSponsored":{"type":"boolean"},"isMeta":{"type":"boolean"},"isGildable":{"type":"boolean"},"isScoreHidden":{"type":"boolean"},"contestMode":{"type":"boolean"},"hidden":{"type":"boolean"},"sendReplies":{"type":"boolean"},"author":{"$ref":"#/components/schemas/RedditAuthor"},"subreddit":{"$ref":"#/components/schemas/RedditSubredditRef"},"flair":{"type":"object","nullable":true},"thumbnail":{"type":"object","additionalProperties":true},"preview":{"type":"object","additionalProperties":true},"awards":{"type":"array","items":{"type":"object","additionalProperties":true}},"flairs":{"type":"array","items":{"type":"object","additionalProperties":true}},"gallery":{"type":"array","items":{"type":"object","additionalProperties":true}},"crosspostParents":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"RedditAuthor":{"type":"object","description":"Lightweight author reference embedded in posts and comments.","properties":{"id":{"type":"string"},"username":{"type":"string"},"isBlocked":{"type":"boolean"},"isDeleted":{"type":"boolean"}}},"RedditSubredditRef":{"type":"object","description":"Lightweight subreddit reference embedded in posts and comments.","properties":{"id":{"type":"string"},"name":{"type":"string"},"prefixedName":{"type":"string"},"type":{"type":"string"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/reddit/subreddit/{name}/posts":{"get":{"operationId":"getRedditSubredditPosts","summary":"Get posts from a subreddit","description":"Returns posts from a subreddit. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.001 per item\n\n**Minimum charge:** $0.01 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["reddit"],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["hot","top","new"]}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/RedditPost"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```


# Instagram

Instagram: users, posts, reels, stories, and comments.

## Resolve user ID from handle

> Returns whether the handle is available and, if it exists, the resolved Instagram user ID.\
> \
> \*\*Price:\*\* $0.0015 per item\
> \
> \*\*Minimum charge:\*\* $0.0015 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"instagram","description":"Instagram: users, posts, reels, stories, and comments."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"InstagramUserIdLookup":{"type":"object","description":"Result of resolving an Instagram user ID from a handle.","properties":{"isAvailable":{"type":"boolean"},"handle":{"type":"string"},"id":{"type":"string","description":"Present only when `isAvailable` is true."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/instagram/userid/{handle}":{"get":{"operationId":"getInstagramUserIdByHandle","summary":"Resolve user ID from handle","description":"Returns whether the handle is available and, if it exists, the resolved Instagram user ID.\n\n**Price:** $0.0015 per item\n\n**Minimum charge:** $0.0015 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["instagram"],"parameters":[{"name":"handle","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/InstagramUserIdLookup"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get a user by handle

> Returns an Instagram user profile by handle (without \`@\`).\
> \
> \*\*Price:\*\* $0.0015 per item\
> \
> \*\*Minimum charge:\*\* $0.0015 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"instagram","description":"Instagram: users, posts, reels, stories, and comments."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"InstagramUser":{"type":"object","description":"An Instagram user profile.","properties":{"type":{"type":"string"},"isAvailable":{"type":"boolean"},"id":{"type":"string"},"username":{"type":"string"},"fullName":{"type":"string"},"biography":{"type":"string"},"profilePicUrl":{"type":"string"},"website":{"type":"string"},"category":{"type":"string"},"cityName":{"type":"string","nullable":true},"isPrivate":{"type":"boolean"},"isVerified":{"type":"boolean"},"isBusiness":{"type":"boolean"},"hasVideos":{"type":"boolean"},"hasGuides":{"type":"boolean"},"postCount":{"type":"integer"},"followerCount":{"type":"integer"},"followingCount":{"type":"integer"},"totalReelsCount":{"type":"integer"},"bioLinks":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/instagram/user/handle/{handle}":{"get":{"operationId":"getInstagramUserByHandle","summary":"Get a user by handle","description":"Returns an Instagram user profile by handle (without `@`).\n\n**Price:** $0.0015 per item\n\n**Minimum charge:** $0.0015 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["instagram"],"parameters":[{"name":"handle","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/InstagramUser"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get a user by ID

> Returns an Instagram user profile by numeric user ID.\
> \
> \*\*Price:\*\* $0.0015 per item\
> \
> \*\*Minimum charge:\*\* $0.0015 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"instagram","description":"Instagram: users, posts, reels, stories, and comments."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"InstagramUser":{"type":"object","description":"An Instagram user profile.","properties":{"type":{"type":"string"},"isAvailable":{"type":"boolean"},"id":{"type":"string"},"username":{"type":"string"},"fullName":{"type":"string"},"biography":{"type":"string"},"profilePicUrl":{"type":"string"},"website":{"type":"string"},"category":{"type":"string"},"cityName":{"type":"string","nullable":true},"isPrivate":{"type":"boolean"},"isVerified":{"type":"boolean"},"isBusiness":{"type":"boolean"},"hasVideos":{"type":"boolean"},"hasGuides":{"type":"boolean"},"postCount":{"type":"integer"},"followerCount":{"type":"integer"},"followingCount":{"type":"integer"},"totalReelsCount":{"type":"integer"},"bioLinks":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/instagram/user/{id}":{"get":{"operationId":"getInstagramUserById","summary":"Get a user by ID","description":"Returns an Instagram user profile by numeric user ID.\n\n**Price:** $0.0015 per item\n\n**Minimum charge:** $0.0015 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["instagram"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/InstagramUser"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get posts published by a user

> Returns posts published by an Instagram user. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.0003 per item\
> \
> \*\*Minimum charge:\*\* $0.003 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"instagram","description":"Instagram: users, posts, reels, stories, and comments."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"InstagramPost":{"type":"object","description":"An Instagram post (image, video, carousel, or reel).","properties":{"type":{"type":"string","description":"photo / video / carousel / reel"},"id":{"type":"string"},"code":{"type":"string"},"url":{"type":"string"},"caption":{"type":"string"},"createdAt":{"type":"integer","description":"Unix timestamp in seconds."},"likeCount":{"type":"integer"},"commentCount":{"type":"integer"},"isAvailable":{"type":"boolean"},"isCarousel":{"type":"boolean"},"isVideo":{"type":"boolean"},"isPinned":{"type":"boolean"},"isPaidPartnership":{"type":"boolean"},"isLikeAndViewCountsDisabled":{"type":"boolean"},"owner":{"$ref":"#/components/schemas/InstagramPostOwner"},"coowners":{"type":"array","items":{"$ref":"#/components/schemas/InstagramPostOwner"}},"location":{"type":"object","nullable":true,"additionalProperties":true},"image":{"type":"object","nullable":true,"additionalProperties":true},"video":{"type":"object","nullable":true,"additionalProperties":true},"audio":{"type":"object","nullable":true,"additionalProperties":true},"carouselMedia":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"InstagramPostOwner":{"type":"object","description":"Lightweight user reference embedded in posts and stories.","properties":{"id":{"type":"string"},"username":{"type":"string"},"fullName":{"type":"string"},"profilePicUrl":{"type":"string"},"isVerified":{"type":"boolean"},"isPrivate":{"type":"boolean"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/instagram/user/{id}/posts":{"get":{"operationId":"getInstagramUserPosts","summary":"Get posts published by a user","description":"Returns posts published by an Instagram user. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.0003 per item\n\n**Minimum charge:** $0.003 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["instagram"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/InstagramPost"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get posts a user is tagged in

> Returns posts where the Instagram user is tagged. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.0003 per item\
> \
> \*\*Minimum charge:\*\* $0.003 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"instagram","description":"Instagram: users, posts, reels, stories, and comments."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"InstagramPost":{"type":"object","description":"An Instagram post (image, video, carousel, or reel).","properties":{"type":{"type":"string","description":"photo / video / carousel / reel"},"id":{"type":"string"},"code":{"type":"string"},"url":{"type":"string"},"caption":{"type":"string"},"createdAt":{"type":"integer","description":"Unix timestamp in seconds."},"likeCount":{"type":"integer"},"commentCount":{"type":"integer"},"isAvailable":{"type":"boolean"},"isCarousel":{"type":"boolean"},"isVideo":{"type":"boolean"},"isPinned":{"type":"boolean"},"isPaidPartnership":{"type":"boolean"},"isLikeAndViewCountsDisabled":{"type":"boolean"},"owner":{"$ref":"#/components/schemas/InstagramPostOwner"},"coowners":{"type":"array","items":{"$ref":"#/components/schemas/InstagramPostOwner"}},"location":{"type":"object","nullable":true,"additionalProperties":true},"image":{"type":"object","nullable":true,"additionalProperties":true},"video":{"type":"object","nullable":true,"additionalProperties":true},"audio":{"type":"object","nullable":true,"additionalProperties":true},"carouselMedia":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"InstagramPostOwner":{"type":"object","description":"Lightweight user reference embedded in posts and stories.","properties":{"id":{"type":"string"},"username":{"type":"string"},"fullName":{"type":"string"},"profilePicUrl":{"type":"string"},"isVerified":{"type":"boolean"},"isPrivate":{"type":"boolean"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/instagram/user/{id}/posts/tagged":{"get":{"operationId":"getInstagramUserTaggedPosts","summary":"Get posts a user is tagged in","description":"Returns posts where the Instagram user is tagged. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.0003 per item\n\n**Minimum charge:** $0.003 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["instagram"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/InstagramPost"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get reels published by a user

> Returns reels published by an Instagram user. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.0003 per item\
> \
> \*\*Minimum charge:\*\* $0.003 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"instagram","description":"Instagram: users, posts, reels, stories, and comments."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"InstagramPost":{"type":"object","description":"An Instagram post (image, video, carousel, or reel).","properties":{"type":{"type":"string","description":"photo / video / carousel / reel"},"id":{"type":"string"},"code":{"type":"string"},"url":{"type":"string"},"caption":{"type":"string"},"createdAt":{"type":"integer","description":"Unix timestamp in seconds."},"likeCount":{"type":"integer"},"commentCount":{"type":"integer"},"isAvailable":{"type":"boolean"},"isCarousel":{"type":"boolean"},"isVideo":{"type":"boolean"},"isPinned":{"type":"boolean"},"isPaidPartnership":{"type":"boolean"},"isLikeAndViewCountsDisabled":{"type":"boolean"},"owner":{"$ref":"#/components/schemas/InstagramPostOwner"},"coowners":{"type":"array","items":{"$ref":"#/components/schemas/InstagramPostOwner"}},"location":{"type":"object","nullable":true,"additionalProperties":true},"image":{"type":"object","nullable":true,"additionalProperties":true},"video":{"type":"object","nullable":true,"additionalProperties":true},"audio":{"type":"object","nullable":true,"additionalProperties":true},"carouselMedia":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"InstagramPostOwner":{"type":"object","description":"Lightweight user reference embedded in posts and stories.","properties":{"id":{"type":"string"},"username":{"type":"string"},"fullName":{"type":"string"},"profilePicUrl":{"type":"string"},"isVerified":{"type":"boolean"},"isPrivate":{"type":"boolean"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/instagram/user/{id}/reels":{"get":{"operationId":"getInstagramUserReels","summary":"Get reels published by a user","description":"Returns reels published by an Instagram user. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.0003 per item\n\n**Minimum charge:** $0.003 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["instagram"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/InstagramPost"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get active stories for a user

> Returns the user's currently-active Instagram stories (non-paginated).\
> \
> \*\*Price:\*\* $0.0003 per item\
> \
> \*\*Minimum charge:\*\* $0.003 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"instagram","description":"Instagram: users, posts, reels, stories, and comments."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"InstagramStory":{"type":"object","description":"A single Instagram story slide.","properties":{"type":{"type":"string","description":"photo / video"},"id":{"type":"string"},"code":{"type":"string"},"createdAt":{"type":"integer","description":"Unix timestamp in seconds."},"expiringAt":{"type":"integer","description":"Unix timestamp in seconds."},"duration":{"type":"number"},"hasAudio":{"type":"boolean"},"hasTranslation":{"type":"boolean"},"canReshare":{"type":"boolean"},"canReply":{"type":"boolean"},"isVideo":{"type":"boolean"},"owner":{"$ref":"#/components/schemas/InstagramPostOwner"},"coowners":{"type":"array","items":{"$ref":"#/components/schemas/InstagramPostOwner"}},"image":{"type":"object","nullable":true,"additionalProperties":true},"video":{"type":"object","nullable":true,"additionalProperties":true},"audio":{"type":"object","nullable":true,"additionalProperties":true},"link":{"type":"object","nullable":true,"additionalProperties":true},"comments":{"type":"array","items":{"type":"object","additionalProperties":true}},"mentions":{"type":"array","items":{"type":"object","additionalProperties":true}},"hashtags":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"InstagramPostOwner":{"type":"object","description":"Lightweight user reference embedded in posts and stories.","properties":{"id":{"type":"string"},"username":{"type":"string"},"fullName":{"type":"string"},"profilePicUrl":{"type":"string"},"isVerified":{"type":"boolean"},"isPrivate":{"type":"boolean"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/instagram/user/{id}/stories":{"get":{"operationId":"getInstagramUserStories","summary":"Get active stories for a user","description":"Returns the user's currently-active Instagram stories (non-paginated).\n\n**Price:** $0.0003 per item\n\n**Minimum charge:** $0.003 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["instagram"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/InstagramStory"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get a post by shortcode

> Returns a single Instagram post by its shortcode (the segment after \`/p/\` in the URL).\
> \
> \*\*Price:\*\* $0.0015 per item\
> \
> \*\*Minimum charge:\*\* $0.0015 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"instagram","description":"Instagram: users, posts, reels, stories, and comments."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"InstagramPost":{"type":"object","description":"An Instagram post (image, video, carousel, or reel).","properties":{"type":{"type":"string","description":"photo / video / carousel / reel"},"id":{"type":"string"},"code":{"type":"string"},"url":{"type":"string"},"caption":{"type":"string"},"createdAt":{"type":"integer","description":"Unix timestamp in seconds."},"likeCount":{"type":"integer"},"commentCount":{"type":"integer"},"isAvailable":{"type":"boolean"},"isCarousel":{"type":"boolean"},"isVideo":{"type":"boolean"},"isPinned":{"type":"boolean"},"isPaidPartnership":{"type":"boolean"},"isLikeAndViewCountsDisabled":{"type":"boolean"},"owner":{"$ref":"#/components/schemas/InstagramPostOwner"},"coowners":{"type":"array","items":{"$ref":"#/components/schemas/InstagramPostOwner"}},"location":{"type":"object","nullable":true,"additionalProperties":true},"image":{"type":"object","nullable":true,"additionalProperties":true},"video":{"type":"object","nullable":true,"additionalProperties":true},"audio":{"type":"object","nullable":true,"additionalProperties":true},"carouselMedia":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"InstagramPostOwner":{"type":"object","description":"Lightweight user reference embedded in posts and stories.","properties":{"id":{"type":"string"},"username":{"type":"string"},"fullName":{"type":"string"},"profilePicUrl":{"type":"string"},"isVerified":{"type":"boolean"},"isPrivate":{"type":"boolean"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/instagram/post/code/{code}":{"get":{"operationId":"getInstagramPostByCode","summary":"Get a post by shortcode","description":"Returns a single Instagram post by its shortcode (the segment after `/p/` in the URL).\n\n**Price:** $0.0015 per item\n\n**Minimum charge:** $0.0015 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["instagram"],"parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/InstagramPost"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get comments on a post

> Returns comments on an Instagram post. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.0003 per item\
> \
> \*\*Minimum charge:\*\* $0.003 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"instagram","description":"Instagram: users, posts, reels, stories, and comments."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"InstagramComment":{"type":"object","description":"A comment on an Instagram post.","properties":{"type":{"type":"string"},"id":{"type":"string"},"userId":{"type":"string"},"message":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"likeCount":{"type":"integer"},"replyCount":{"type":"integer"},"isRanked":{"type":"boolean"},"user":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"string"},"username":{"type":"string"},"fullName":{"type":"string"},"profilePicUrl":{"type":"string"},"isPrivate":{"type":"boolean"},"isVerified":{"type":"boolean"}}}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/instagram/post/{id}/comments":{"get":{"operationId":"getInstagramPostComments","summary":"Get comments on a post","description":"Returns comments on an Instagram post. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.0003 per item\n\n**Minimum charge:** $0.003 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["instagram"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/InstagramComment"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```


# Tiktok

TikTok: users, posts, comments, hashtags, and search.

## Get a user by handle

> Returns a TikTok user profile by handle (without \`@\`).\
> \
> \*\*Price:\*\* $0.0015 per item\
> \
> \*\*Minimum charge:\*\* $0.0015 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"tiktok","description":"TikTok: users, posts, comments, hashtags, and search."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"TiktokUser":{"type":"object","description":"A TikTok user profile.","properties":{"id":{"type":"string"},"username":{"type":"string"},"nickname":{"type":"string"},"url":{"type":"string"},"bio":{"type":"string"},"bioUrl":{"type":"string"},"avatar":{"type":"string"},"verified":{"type":"boolean"},"profileCategory":{"type":"string"},"followers":{"type":"integer"},"following":{"type":"integer"},"likes":{"type":"integer"},"videos":{"type":"integer"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/tiktok/user/handle/{username}":{"get":{"operationId":"getTiktokUserByUsername","summary":"Get a user by handle","description":"Returns a TikTok user profile by handle (without `@`).\n\n**Price:** $0.0015 per item\n\n**Minimum charge:** $0.0015 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["tiktok"],"parameters":[{"name":"username","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TiktokUser"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get posts published by a user

> Returns posts published by a TikTok user. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive). \`region\` may bias results toward a specific country (e.g. \`US\`).\
> \
> \*\*Price:\*\* $0.0003 per item\
> \
> \*\*Minimum charge:\*\* $0.003 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"tiktok","description":"TikTok: users, posts, comments, hashtags, and search."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"TiktokPost":{"type":"object","description":"A TikTok post (video).","properties":{"id":{"type":"string"},"title":{"type":"string"},"postPage":{"type":"string"},"uploadedAt":{"type":"integer","description":"Unix timestamp in seconds."},"uploadedAtFormatted":{"type":"string","format":"date-time"},"views":{"type":"integer"},"likes":{"type":"integer"},"comments":{"type":"integer"},"shares":{"type":"integer"},"bookmarks":{"type":"integer"},"poi":{"type":"object","nullable":true},"channel":{"$ref":"#/components/schemas/TiktokPostChannel"},"collabInfo":{"type":"object","additionalProperties":true},"video":{"type":"object","additionalProperties":true},"song":{"type":"object","additionalProperties":true},"hashtags":{"type":"array","items":{"type":"object","additionalProperties":true}},"subtitleInformation":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"TiktokPostChannel":{"type":"object","description":"Lightweight user reference embedded in posts (post's author / channel).","properties":{"id":{"type":"string"},"username":{"type":"string"},"name":{"type":"string"},"bio":{"type":"string"},"avatar":{"type":"string"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/tiktok/user/{id}/posts":{"get":{"operationId":"getTiktokUserPosts","summary":"Get posts published by a user","description":"Returns posts published by a TikTok user. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive). `region` may bias results toward a specific country (e.g. `US`).\n\n**Price:** $0.0003 per item\n\n**Minimum charge:** $0.003 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["tiktok"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"region","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":5}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TiktokPost"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get a post by ID

> Returns a single TikTok post by its numeric ID.\
> \
> \*\*Price:\*\* $0.0015 per item\
> \
> \*\*Minimum charge:\*\* $0.0015 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"tiktok","description":"TikTok: users, posts, comments, hashtags, and search."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"TiktokPost":{"type":"object","description":"A TikTok post (video).","properties":{"id":{"type":"string"},"title":{"type":"string"},"postPage":{"type":"string"},"uploadedAt":{"type":"integer","description":"Unix timestamp in seconds."},"uploadedAtFormatted":{"type":"string","format":"date-time"},"views":{"type":"integer"},"likes":{"type":"integer"},"comments":{"type":"integer"},"shares":{"type":"integer"},"bookmarks":{"type":"integer"},"poi":{"type":"object","nullable":true},"channel":{"$ref":"#/components/schemas/TiktokPostChannel"},"collabInfo":{"type":"object","additionalProperties":true},"video":{"type":"object","additionalProperties":true},"song":{"type":"object","additionalProperties":true},"hashtags":{"type":"array","items":{"type":"object","additionalProperties":true}},"subtitleInformation":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"TiktokPostChannel":{"type":"object","description":"Lightweight user reference embedded in posts (post's author / channel).","properties":{"id":{"type":"string"},"username":{"type":"string"},"name":{"type":"string"},"bio":{"type":"string"},"avatar":{"type":"string"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/tiktok/post/{id}":{"get":{"operationId":"getTiktokPostById","summary":"Get a post by ID","description":"Returns a single TikTok post by its numeric ID.\n\n**Price:** $0.0015 per item\n\n**Minimum charge:** $0.0015 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["tiktok"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":50}},{"name":"region","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":5}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TiktokPost"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get comments on a post

> Returns comments on a TikTok post. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.0003 per item\
> \
> \*\*Minimum charge:\*\* $0.003 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"tiktok","description":"TikTok: users, posts, comments, hashtags, and search."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"TiktokComment":{"type":"object","description":"A comment or reply on a TikTok post.","properties":{"id":{"type":"string"},"parentId":{"type":"string","description":"Present only on replies — references the parent comment ID."},"awemeId":{"type":"string","description":"ID of the post the comment was made on."},"text":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"likeCount":{"type":"integer"},"replyCount":{"type":"integer","description":"Present only on top-level comments."},"commentLanguage":{"type":"string"},"isAuthorLiked":{"type":"boolean"},"user":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"string"},"username":{"type":"string"},"nickname":{"type":"string"},"avatar":{"type":"string"},"verified":{"type":"boolean"}}}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/tiktok/post/{id}/comments":{"get":{"operationId":"getTiktokPostComments","summary":"Get comments on a post","description":"Returns comments on a TikTok post. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.0003 per item\n\n**Minimum charge:** $0.003 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["tiktok"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TiktokComment"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get replies to a comment

> Returns replies to a specific TikTok comment. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.0003 per item\
> \
> \*\*Minimum charge:\*\* $0.003 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"tiktok","description":"TikTok: users, posts, comments, hashtags, and search."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"TiktokComment":{"type":"object","description":"A comment or reply on a TikTok post.","properties":{"id":{"type":"string"},"parentId":{"type":"string","description":"Present only on replies — references the parent comment ID."},"awemeId":{"type":"string","description":"ID of the post the comment was made on."},"text":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"likeCount":{"type":"integer"},"replyCount":{"type":"integer","description":"Present only on top-level comments."},"commentLanguage":{"type":"string"},"isAuthorLiked":{"type":"boolean"},"user":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"string"},"username":{"type":"string"},"nickname":{"type":"string"},"avatar":{"type":"string"},"verified":{"type":"boolean"}}}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/tiktok/post/{id}/comments/{commentId}/replies":{"get":{"operationId":"getTiktokCommentReplies","summary":"Get replies to a comment","description":"Returns replies to a specific TikTok comment. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.0003 per item\n\n**Minimum charge:** $0.003 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["tiktok"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"commentId","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TiktokComment"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Search TikTok posts

> Search TikTok posts by keyword. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.0003 per item\
> \
> \*\*Minimum charge:\*\* $0.003 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"tiktok","description":"TikTok: users, posts, comments, hashtags, and search."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"TiktokPost":{"type":"object","description":"A TikTok post (video).","properties":{"id":{"type":"string"},"title":{"type":"string"},"postPage":{"type":"string"},"uploadedAt":{"type":"integer","description":"Unix timestamp in seconds."},"uploadedAtFormatted":{"type":"string","format":"date-time"},"views":{"type":"integer"},"likes":{"type":"integer"},"comments":{"type":"integer"},"shares":{"type":"integer"},"bookmarks":{"type":"integer"},"poi":{"type":"object","nullable":true},"channel":{"$ref":"#/components/schemas/TiktokPostChannel"},"collabInfo":{"type":"object","additionalProperties":true},"video":{"type":"object","additionalProperties":true},"song":{"type":"object","additionalProperties":true},"hashtags":{"type":"array","items":{"type":"object","additionalProperties":true}},"subtitleInformation":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"TiktokPostChannel":{"type":"object","description":"Lightweight user reference embedded in posts (post's author / channel).","properties":{"id":{"type":"string"},"username":{"type":"string"},"name":{"type":"string"},"bio":{"type":"string"},"avatar":{"type":"string"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/tiktok/post/search":{"get":{"operationId":"searchTiktokPosts","summary":"Search TikTok posts","description":"Search TikTok posts by keyword. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.0003 per item\n\n**Minimum charge:** $0.003 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["tiktok"],"parameters":[{"name":"keyword","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"region","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":5}},{"name":"dateRange","in":"query","required":false,"schema":{"type":"string"}},{"name":"sortType","in":"query","required":false,"schema":{"type":"string"}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TiktokPost"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get a post by URL

> Returns a single TikTok post by its full URL.\
> \
> \*\*Price:\*\* $0.0015 per item\
> \
> \*\*Minimum charge:\*\* $0.0015 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"tiktok","description":"TikTok: users, posts, comments, hashtags, and search."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"TiktokPost":{"type":"object","description":"A TikTok post (video).","properties":{"id":{"type":"string"},"title":{"type":"string"},"postPage":{"type":"string"},"uploadedAt":{"type":"integer","description":"Unix timestamp in seconds."},"uploadedAtFormatted":{"type":"string","format":"date-time"},"views":{"type":"integer"},"likes":{"type":"integer"},"comments":{"type":"integer"},"shares":{"type":"integer"},"bookmarks":{"type":"integer"},"poi":{"type":"object","nullable":true},"channel":{"$ref":"#/components/schemas/TiktokPostChannel"},"collabInfo":{"type":"object","additionalProperties":true},"video":{"type":"object","additionalProperties":true},"song":{"type":"object","additionalProperties":true},"hashtags":{"type":"array","items":{"type":"object","additionalProperties":true}},"subtitleInformation":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"TiktokPostChannel":{"type":"object","description":"Lightweight user reference embedded in posts (post's author / channel).","properties":{"id":{"type":"string"},"username":{"type":"string"},"name":{"type":"string"},"bio":{"type":"string"},"avatar":{"type":"string"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/tiktok/post":{"get":{"operationId":"getTiktokPostByUrl","summary":"Get a post by URL","description":"Returns a single TikTok post by its full URL.\n\n**Price:** $0.0015 per item\n\n**Minimum charge:** $0.0015 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["tiktok"],"parameters":[{"name":"url","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":2048,"format":"uri"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TiktokPost"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get a hashtag by name

> Returns a TikTok hashtag profile by name (without \`#\`).\
> \
> \*\*Price:\*\* $0.0015 per item\
> \
> \*\*Minimum charge:\*\* $0.0015 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"tiktok","description":"TikTok: users, posts, comments, hashtags, and search."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"TiktokHashtag":{"type":"object","description":"A TikTok hashtag profile.","properties":{"id":{"type":"string"},"name":{"type":"string"},"views":{"type":"integer"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/tiktok/hashtag/handle/{name}":{"get":{"operationId":"getTiktokHashtagByName","summary":"Get a hashtag by name","description":"Returns a TikTok hashtag profile by name (without `#`).\n\n**Price:** $0.0015 per item\n\n**Minimum charge:** $0.0015 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["tiktok"],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TiktokHashtag"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```


# Twitter

Twitter: tweets, users, handles, and search.

## Search Twitter

> Search tweets by query. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive). \`sort\` is \`Top\` or \`Latest\` (default \`Latest\`).\
> \
> \*\*Price:\*\* $0.00015 per item\
> \
> \*\*Minimum charge:\*\* $0.0015 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"twitter","description":"Twitter: tweets, users, handles, and search."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"TwitterTweet":{"type":"object","description":"A single tweet.","properties":{"type":{"type":"string"},"id":{"type":"string"},"url":{"type":"string"},"twitterUrl":{"type":"string"},"text":{"type":"string"},"fullText":{"type":"string"},"source":{"type":"string"},"lang":{"type":"string"},"createdAt":{"type":"string"},"conversationId":{"type":"string"},"retweetCount":{"type":"integer"},"replyCount":{"type":"integer"},"likeCount":{"type":"integer"},"quoteCount":{"type":"integer"},"bookmarkCount":{"type":"integer"},"viewCount":{"type":"integer","nullable":true},"isReply":{"type":"boolean"},"isPinned":{"type":"boolean"},"isRetweet":{"type":"boolean"},"isQuote":{"type":"boolean"},"author":{"$ref":"#/components/schemas/TwitterUser"},"media":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"TwitterUser":{"type":"object","description":"A Twitter user profile.","properties":{"type":{"type":"string"},"id":{"type":"string"},"userName":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"twitterUrl":{"type":"string"},"description":{"type":"string"},"location":{"type":"string"},"profilePicture":{"type":"string"},"coverPicture":{"type":"string"},"isVerified":{"type":"boolean"},"isBlueVerified":{"type":"boolean","nullable":true},"protected":{"type":"boolean"},"canDm":{"type":"boolean"},"canMediaTag":{"type":"boolean"},"followers":{"type":"integer"},"following":{"type":"integer"},"fastFollowersCount":{"type":"integer"},"favouritesCount":{"type":"integer"},"listedCount":{"type":"integer"},"mediaCount":{"type":"integer"},"statusesCount":{"type":"integer"},"createdAt":{"type":"string"},"status":{"type":"string"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/twitter/search":{"get":{"operationId":"searchTwitter","summary":"Search Twitter","description":"Search tweets by query. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive). `sort` is `Top` or `Latest` (default `Latest`).\n\n**Price:** $0.00015 per item\n\n**Minimum charge:** $0.0015 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["twitter"],"parameters":[{"name":"query","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"sort","in":"query","required":false,"schema":{"default":"Latest","type":"string","enum":["Top","Latest"]}},{"name":"useFirst","in":"query","required":false,"schema":{"default":"false","type":"string","enum":["true","false"]}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TwitterTweet"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get a tweet by ID

> Returns a single tweet by its numeric ID.\
> \
> \*\*Price:\*\* $0.00075 per item\
> \
> \*\*Minimum charge:\*\* $0.00075 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"twitter","description":"Twitter: tweets, users, handles, and search."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"TwitterTweet":{"type":"object","description":"A single tweet.","properties":{"type":{"type":"string"},"id":{"type":"string"},"url":{"type":"string"},"twitterUrl":{"type":"string"},"text":{"type":"string"},"fullText":{"type":"string"},"source":{"type":"string"},"lang":{"type":"string"},"createdAt":{"type":"string"},"conversationId":{"type":"string"},"retweetCount":{"type":"integer"},"replyCount":{"type":"integer"},"likeCount":{"type":"integer"},"quoteCount":{"type":"integer"},"bookmarkCount":{"type":"integer"},"viewCount":{"type":"integer","nullable":true},"isReply":{"type":"boolean"},"isPinned":{"type":"boolean"},"isRetweet":{"type":"boolean"},"isQuote":{"type":"boolean"},"author":{"$ref":"#/components/schemas/TwitterUser"},"media":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"TwitterUser":{"type":"object","description":"A Twitter user profile.","properties":{"type":{"type":"string"},"id":{"type":"string"},"userName":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"twitterUrl":{"type":"string"},"description":{"type":"string"},"location":{"type":"string"},"profilePicture":{"type":"string"},"coverPicture":{"type":"string"},"isVerified":{"type":"boolean"},"isBlueVerified":{"type":"boolean","nullable":true},"protected":{"type":"boolean"},"canDm":{"type":"boolean"},"canMediaTag":{"type":"boolean"},"followers":{"type":"integer"},"following":{"type":"integer"},"fastFollowersCount":{"type":"integer"},"favouritesCount":{"type":"integer"},"listedCount":{"type":"integer"},"mediaCount":{"type":"integer"},"statusesCount":{"type":"integer"},"createdAt":{"type":"string"},"status":{"type":"string"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/twitter/tweet/{id}":{"get":{"operationId":"getTwitterTweet","summary":"Get a tweet by ID","description":"Returns a single tweet by its numeric ID.\n\n**Price:** $0.00075 per item\n\n**Minimum charge:** $0.00075 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["twitter"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TwitterTweet"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get a user by handle

> Returns a Twitter user by handle (without \`@\`).\
> \
> \*\*Price:\*\* $0.00075 per item\
> \
> \*\*Minimum charge:\*\* $0.00075 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"twitter","description":"Twitter: tweets, users, handles, and search."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"TwitterUser":{"type":"object","description":"A Twitter user profile.","properties":{"type":{"type":"string"},"id":{"type":"string"},"userName":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"twitterUrl":{"type":"string"},"description":{"type":"string"},"location":{"type":"string"},"profilePicture":{"type":"string"},"coverPicture":{"type":"string"},"isVerified":{"type":"boolean"},"isBlueVerified":{"type":"boolean","nullable":true},"protected":{"type":"boolean"},"canDm":{"type":"boolean"},"canMediaTag":{"type":"boolean"},"followers":{"type":"integer"},"following":{"type":"integer"},"fastFollowersCount":{"type":"integer"},"favouritesCount":{"type":"integer"},"listedCount":{"type":"integer"},"mediaCount":{"type":"integer"},"statusesCount":{"type":"integer"},"createdAt":{"type":"string"},"status":{"type":"string"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/twitter/handle/{handle}":{"get":{"operationId":"getTwitterUserByHandle","summary":"Get a user by handle","description":"Returns a Twitter user by handle (without `@`).\n\n**Price:** $0.00075 per item\n\n**Minimum charge:** $0.00075 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["twitter"],"parameters":[{"name":"handle","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TwitterUser"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get a user about by handle

> Returns the about/bio details for a Twitter user by handle.\
> \
> \*\*Price:\*\* $0.00075 per item\
> \
> \*\*Minimum charge:\*\* $0.00075 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"twitter","description":"Twitter: tweets, users, handles, and search."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"TwitterUserAbout":{"type":"object","description":"Extended profile / about details for a Twitter user.","properties":{"accountBasedIn":{"type":"string"},"locationAccurate":{"type":"boolean"},"profileImageShape":{"type":"string"},"avatarUrl":{"type":"string"},"usernameChangeCount":{"type":"string"},"accountCreatedAt":{"type":"string"},"accountSource":{"type":"string"},"verifiedSince":{"type":"string"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/twitter/handle/{handle}/about":{"get":{"operationId":"getTwitterUserAbout","summary":"Get a user about by handle","description":"Returns the about/bio details for a Twitter user by handle.\n\n**Price:** $0.00075 per item\n\n**Minimum charge:** $0.00075 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["twitter"],"parameters":[{"name":"handle","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TwitterUserAbout"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get a user by ID

> Returns a Twitter user by their numeric user ID.\
> \
> \*\*Price:\*\* $0.00075 per item\
> \
> \*\*Minimum charge:\*\* $0.00075 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"twitter","description":"Twitter: tweets, users, handles, and search."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"TwitterUser":{"type":"object","description":"A Twitter user profile.","properties":{"type":{"type":"string"},"id":{"type":"string"},"userName":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"twitterUrl":{"type":"string"},"description":{"type":"string"},"location":{"type":"string"},"profilePicture":{"type":"string"},"coverPicture":{"type":"string"},"isVerified":{"type":"boolean"},"isBlueVerified":{"type":"boolean","nullable":true},"protected":{"type":"boolean"},"canDm":{"type":"boolean"},"canMediaTag":{"type":"boolean"},"followers":{"type":"integer"},"following":{"type":"integer"},"fastFollowersCount":{"type":"integer"},"favouritesCount":{"type":"integer"},"listedCount":{"type":"integer"},"mediaCount":{"type":"integer"},"statusesCount":{"type":"integer"},"createdAt":{"type":"string"},"status":{"type":"string"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/twitter/user/{id}":{"get":{"operationId":"getTwitterUserById","summary":"Get a user by ID","description":"Returns a Twitter user by their numeric user ID.\n\n**Price:** $0.00075 per item\n\n**Minimum charge:** $0.00075 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["twitter"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TwitterUser"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get tweets posted by a user

> Returns tweets posted by a user, by user ID. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.00015 per item\
> \
> \*\*Minimum charge:\*\* $0.0015 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"twitter","description":"Twitter: tweets, users, handles, and search."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"TwitterTweet":{"type":"object","description":"A single tweet.","properties":{"type":{"type":"string"},"id":{"type":"string"},"url":{"type":"string"},"twitterUrl":{"type":"string"},"text":{"type":"string"},"fullText":{"type":"string"},"source":{"type":"string"},"lang":{"type":"string"},"createdAt":{"type":"string"},"conversationId":{"type":"string"},"retweetCount":{"type":"integer"},"replyCount":{"type":"integer"},"likeCount":{"type":"integer"},"quoteCount":{"type":"integer"},"bookmarkCount":{"type":"integer"},"viewCount":{"type":"integer","nullable":true},"isReply":{"type":"boolean"},"isPinned":{"type":"boolean"},"isRetweet":{"type":"boolean"},"isQuote":{"type":"boolean"},"author":{"$ref":"#/components/schemas/TwitterUser"},"media":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"TwitterUser":{"type":"object","description":"A Twitter user profile.","properties":{"type":{"type":"string"},"id":{"type":"string"},"userName":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"twitterUrl":{"type":"string"},"description":{"type":"string"},"location":{"type":"string"},"profilePicture":{"type":"string"},"coverPicture":{"type":"string"},"isVerified":{"type":"boolean"},"isBlueVerified":{"type":"boolean","nullable":true},"protected":{"type":"boolean"},"canDm":{"type":"boolean"},"canMediaTag":{"type":"boolean"},"followers":{"type":"integer"},"following":{"type":"integer"},"fastFollowersCount":{"type":"integer"},"favouritesCount":{"type":"integer"},"listedCount":{"type":"integer"},"mediaCount":{"type":"integer"},"statusesCount":{"type":"integer"},"createdAt":{"type":"string"},"status":{"type":"string"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/twitter/user/{id}/tweets":{"get":{"operationId":"getTwitterUserTweets","summary":"Get tweets posted by a user","description":"Returns tweets posted by a user, by user ID. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.00015 per item\n\n**Minimum charge:** $0.0015 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["twitter"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TwitterTweet"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```


# Yelp

Yelp: places, reviews, and search.

## Get a place by ID

> Returns a Yelp place by its Yelp business ID.\
> \
> \*\*Price:\*\* $0.001 per item\
> \
> \*\*Minimum charge:\*\* $0.001 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"yelp","description":"Yelp: places, reviews, and search."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"YelpPlace":{"type":"object","description":"A Yelp place / business profile.","properties":{"id":{"type":"string"},"alias":{"type":"string"},"title":{"type":"string"},"website":{"type":"string"},"phoneNumber":{"type":"string"},"avgRating":{"type":"number"},"unroundedAvgRating":{"type":"number"},"reviewCount":{"type":"integer"},"priceRange":{"type":"string"},"isBusinessClosed":{"type":"boolean"},"primaryPhoto":{"type":"string"},"mediaCount":{"type":"integer"},"address":{"type":"object","additionalProperties":true},"coordinates":{"type":"object","properties":{"latitude":{"type":"number"},"longitude":{"type":"number"}}},"categories":{"type":"array","items":{"type":"object","additionalProperties":true}},"operationHours":{"type":"array","items":{"type":"object","additionalProperties":true}},"amenities":{"type":"array","items":{"type":"object","additionalProperties":true}},"about":{"type":"object","additionalProperties":true}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/yelp/place/{id}":{"get":{"operationId":"getYelpPlaceById","summary":"Get a place by ID","description":"Returns a Yelp place by its Yelp business ID.\n\n**Price:** $0.001 per item\n\n**Minimum charge:** $0.001 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["yelp"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":200}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/YelpPlace"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get reviews for a place

> Returns reviews for a Yelp place. Use \`page\` (1-indexed) to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.0002 per item\
> \
> \*\*Minimum charge:\*\* $0.002 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"yelp","description":"Yelp: places, reviews, and search."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"YelpReview":{"type":"object","description":"A Yelp review for a place.","properties":{"id":{"type":"string"},"url":{"type":"string"},"rating":{"type":"number"},"text":{"type":"string"},"language":{"type":"string"},"date":{"type":"string","format":"date-time"},"images":{"type":"array","items":{"type":"string"}},"feedback":{"type":"object","additionalProperties":true},"reviewer":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"string"},"name":{"type":"string"},"profileUrl":{"type":"string"},"image":{"type":"string"}}}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/yelp/place/{id}/reviews":{"get":{"operationId":"getYelpPlaceReviews","summary":"Get reviews for a place","description":"Returns reviews for a Yelp place. Use `page` (1-indexed) to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.0002 per item\n\n**Minimum charge:** $0.002 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["yelp"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":200}},{"name":"sortBy","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":50}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/YelpReview"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get a place by handle

> Returns a Yelp place by its handle / slug.\
> \
> \*\*Price:\*\* $0.001 per item\
> \
> \*\*Minimum charge:\*\* $0.001 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"yelp","description":"Yelp: places, reviews, and search."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"YelpPlace":{"type":"object","description":"A Yelp place / business profile.","properties":{"id":{"type":"string"},"alias":{"type":"string"},"title":{"type":"string"},"website":{"type":"string"},"phoneNumber":{"type":"string"},"avgRating":{"type":"number"},"unroundedAvgRating":{"type":"number"},"reviewCount":{"type":"integer"},"priceRange":{"type":"string"},"isBusinessClosed":{"type":"boolean"},"primaryPhoto":{"type":"string"},"mediaCount":{"type":"integer"},"address":{"type":"object","additionalProperties":true},"coordinates":{"type":"object","properties":{"latitude":{"type":"number"},"longitude":{"type":"number"}}},"categories":{"type":"array","items":{"type":"object","additionalProperties":true}},"operationHours":{"type":"array","items":{"type":"object","additionalProperties":true}},"amenities":{"type":"array","items":{"type":"object","additionalProperties":true}},"about":{"type":"object","additionalProperties":true}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/yelp/place/handle/{handle}":{"get":{"operationId":"getYelpPlaceByHandle","summary":"Get a place by handle","description":"Returns a Yelp place by its handle / slug.\n\n**Price:** $0.001 per item\n\n**Minimum charge:** $0.001 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["yelp"],"parameters":[{"name":"handle","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":200}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/YelpPlace"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Search Yelp places

> Search Yelp places by query and location. Use \`page\` (1-indexed) to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.0002 per item\
> \
> \*\*Minimum charge:\*\* $0.002 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"yelp","description":"Yelp: places, reviews, and search."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"YelpSearchResult":{"type":"object","description":"A Yelp search result entry (place summary).","properties":{"id":{"type":"string"},"alias":{"type":"string"},"name":{"type":"string"},"rating":{"type":"number"},"reviewCount":{"type":"integer"},"priceRange":{"type":"string"},"phone":{"type":"string","nullable":true},"website":{"type":"string","nullable":true},"coordinates":{"type":"object","properties":{"latitude":{"type":"number"},"longitude":{"type":"number"}}},"categories":{"type":"array","items":{"type":"object","additionalProperties":true}},"images":{"type":"array","items":{"type":"string"}}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/yelp/search":{"get":{"operationId":"searchYelpPlaces","summary":"Search Yelp places","description":"Search Yelp places by query and location. Use `page` (1-indexed) to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.0002 per item\n\n**Minimum charge:** $0.002 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["yelp"],"parameters":[{"name":"query","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":200}},{"name":"location","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":200}},{"name":"sortBy","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":50}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/YelpSearchResult"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```


# Youtube

YouTube: channels, videos, shorts, live streams, comments, search, trending, hashtags.

## Get YouTube channel details

> Returns channel details. Provide exactly one of \`id\`, \`handle\`, or \`channelUrl\`.\
> \
> \*\*Price:\*\* $0.0015 per item\
> \
> \*\*Minimum charge:\*\* $0.0015 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"youtube","description":"YouTube: channels, videos, shorts, live streams, comments, search, trending, hashtags."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"YoutubeChannel":{"type":"object","properties":{"type":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"handle":{"type":"string"},"url":{"type":"string","format":"uri"},"description":{"type":"string"},"thumbnails":{"type":"string","format":"uri"},"isFamilySafe":{"type":"boolean"},"subscriberCount":{"type":"string"},"availableCountries":{"type":"array","items":{"type":"string"}},"tags":{"type":"array","items":{"type":"string"}},"joinedAt":{"type":"string"},"viewCount":{"type":"string"},"videoCount":{"type":"integer"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/youtube/channel":{"get":{"operationId":"getYoutubeChannel","summary":"Get YouTube channel details","description":"Returns channel details. Provide exactly one of `id`, `handle`, or `channelUrl`.\n\n**Price:** $0.0015 per item\n\n**Minimum charge:** $0.0015 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["youtube"],"parameters":[{"name":"id","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"handle","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"channelUrl","in":"query","required":false,"schema":{"type":"string","minLength":10,"maxLength":1000,"format":"uri"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeChannel"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get videos for a YouTube channel

> Returns videos uploaded by the channel. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.0003 per item\
> \
> \*\*Minimum charge:\*\* $0.003 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"youtube","description":"YouTube: channels, videos, shorts, live streams, comments, search, trending, hashtags."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"YoutubeVideo":{"type":"object","description":"Video result from search, hashtag, or trending endpoints.","properties":{"type":{"type":"string"},"id":{"type":"string"},"url":{"type":"string","format":"uri"},"title":{"type":"string"},"description":{"type":"string"},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}},"richThumbnail":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}},"channel":{"$ref":"#/components/schemas/YoutubeChannelRef"},"publishDate":{"type":"string"},"views":{"type":"string"},"duration":{"type":"string"}}},"YoutubeThumbnail":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"width":{"type":"integer"},"height":{"type":"integer"}}},"YoutubeChannelRef":{"type":"object","description":"Lightweight channel reference embedded in video or comment results.","properties":{"id":{"type":"string"},"name":{"type":"string"},"handle":{"type":"string"},"url":{"type":"string","format":"uri"},"subscriberCount":{"type":"string"},"badges":{"type":"array","items":{"type":"string"}},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/youtube/channel/{id}/videos":{"get":{"operationId":"getYoutubeChannelVideos","summary":"Get videos for a YouTube channel","description":"Returns videos uploaded by the channel. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.0003 per item\n\n**Minimum charge:** $0.003 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["youtube"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeVideo"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get shorts for a YouTube channel

> Returns shorts uploaded by the channel. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.0003 per item\
> \
> \*\*Minimum charge:\*\* $0.003 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"youtube","description":"YouTube: channels, videos, shorts, live streams, comments, search, trending, hashtags."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"YoutubeVideo":{"type":"object","description":"Video result from search, hashtag, or trending endpoints.","properties":{"type":{"type":"string"},"id":{"type":"string"},"url":{"type":"string","format":"uri"},"title":{"type":"string"},"description":{"type":"string"},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}},"richThumbnail":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}},"channel":{"$ref":"#/components/schemas/YoutubeChannelRef"},"publishDate":{"type":"string"},"views":{"type":"string"},"duration":{"type":"string"}}},"YoutubeThumbnail":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"width":{"type":"integer"},"height":{"type":"integer"}}},"YoutubeChannelRef":{"type":"object","description":"Lightweight channel reference embedded in video or comment results.","properties":{"id":{"type":"string"},"name":{"type":"string"},"handle":{"type":"string"},"url":{"type":"string","format":"uri"},"subscriberCount":{"type":"string"},"badges":{"type":"array","items":{"type":"string"}},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/youtube/channel/{id}/shorts":{"get":{"operationId":"getYoutubeChannelShorts","summary":"Get shorts for a YouTube channel","description":"Returns shorts uploaded by the channel. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.0003 per item\n\n**Minimum charge:** $0.003 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["youtube"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeVideo"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get live streams for a YouTube channel

> Returns live streams (current and past) for the channel. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.0003 per item\
> \
> \*\*Minimum charge:\*\* $0.003 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"youtube","description":"YouTube: channels, videos, shorts, live streams, comments, search, trending, hashtags."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"YoutubeVideo":{"type":"object","description":"Video result from search, hashtag, or trending endpoints.","properties":{"type":{"type":"string"},"id":{"type":"string"},"url":{"type":"string","format":"uri"},"title":{"type":"string"},"description":{"type":"string"},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}},"richThumbnail":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}},"channel":{"$ref":"#/components/schemas/YoutubeChannelRef"},"publishDate":{"type":"string"},"views":{"type":"string"},"duration":{"type":"string"}}},"YoutubeThumbnail":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"width":{"type":"integer"},"height":{"type":"integer"}}},"YoutubeChannelRef":{"type":"object","description":"Lightweight channel reference embedded in video or comment results.","properties":{"id":{"type":"string"},"name":{"type":"string"},"handle":{"type":"string"},"url":{"type":"string","format":"uri"},"subscriberCount":{"type":"string"},"badges":{"type":"array","items":{"type":"string"}},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/youtube/channel/{id}/live-streams":{"get":{"operationId":"getYoutubeChannelLiveStreams","summary":"Get live streams for a YouTube channel","description":"Returns live streams (current and past) for the channel. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.0003 per item\n\n**Minimum charge:** $0.003 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["youtube"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeVideo"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get YouTube video details

> Returns full metadata for a single video by its 11-character video \`id\`.\
> \
> \*\*Price:\*\* $0.0015 per item\
> \
> \*\*Minimum charge:\*\* $0.0015 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"youtube","description":"YouTube: channels, videos, shorts, live streams, comments, search, trending, hashtags."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"YoutubeVideoDetail":{"type":"object","description":"Full video metadata returned by /youtube/video.","properties":{"type":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"status":{"type":"string"},"url":{"type":"string","format":"uri"},"description":{"type":"string"},"duration":{"type":"integer","description":"Length in seconds."},"views":{"type":"integer"},"likes":{"type":"integer"},"comments":{"type":"integer"},"channel":{"$ref":"#/components/schemas/YoutubeChannelRef"},"keywords":{"type":"array","items":{"type":"string"}},"superTitle":{"type":"string"},"category":{"type":"string"},"isRatingsAllowed":{"type":"boolean"},"isLive":{"type":"boolean"},"isFamilySafe":{"type":"boolean"},"isPrivate":{"type":"boolean"},"isExclusive":{"type":"boolean"},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}}}},"YoutubeChannelRef":{"type":"object","description":"Lightweight channel reference embedded in video or comment results.","properties":{"id":{"type":"string"},"name":{"type":"string"},"handle":{"type":"string"},"url":{"type":"string","format":"uri"},"subscriberCount":{"type":"string"},"badges":{"type":"array","items":{"type":"string"}},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}}}},"YoutubeThumbnail":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"width":{"type":"integer"},"height":{"type":"integer"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/youtube/video":{"get":{"operationId":"getYoutubeVideo","summary":"Get YouTube video details","description":"Returns full metadata for a single video by its 11-character video `id`.\n\n**Price:** $0.0015 per item\n\n**Minimum charge:** $0.0015 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["youtube"],"parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string","minLength":5,"maxLength":20}},{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["shorts","video"]}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeVideoDetail"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get YouTube short details

> Returns full metadata for a single short by its video \`id\`.\
> \
> \*\*Price:\*\* $0.0015 per item\
> \
> \*\*Minimum charge:\*\* $0.0015 per request (1 item)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"youtube","description":"YouTube: channels, videos, shorts, live streams, comments, search, trending, hashtags."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"YoutubeVideoDetail":{"type":"object","description":"Full video metadata returned by /youtube/video.","properties":{"type":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"status":{"type":"string"},"url":{"type":"string","format":"uri"},"description":{"type":"string"},"duration":{"type":"integer","description":"Length in seconds."},"views":{"type":"integer"},"likes":{"type":"integer"},"comments":{"type":"integer"},"channel":{"$ref":"#/components/schemas/YoutubeChannelRef"},"keywords":{"type":"array","items":{"type":"string"}},"superTitle":{"type":"string"},"category":{"type":"string"},"isRatingsAllowed":{"type":"boolean"},"isLive":{"type":"boolean"},"isFamilySafe":{"type":"boolean"},"isPrivate":{"type":"boolean"},"isExclusive":{"type":"boolean"},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}}}},"YoutubeChannelRef":{"type":"object","description":"Lightweight channel reference embedded in video or comment results.","properties":{"id":{"type":"string"},"name":{"type":"string"},"handle":{"type":"string"},"url":{"type":"string","format":"uri"},"subscriberCount":{"type":"string"},"badges":{"type":"array","items":{"type":"string"}},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}}}},"YoutubeThumbnail":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"width":{"type":"integer"},"height":{"type":"integer"}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/youtube/shorts/{id}":{"get":{"operationId":"getYoutubeShorts","summary":"Get YouTube short details","description":"Returns full metadata for a single short by its video `id`.\n\n**Price:** $0.0015 per item\n\n**Minimum charge:** $0.0015 per request (1 item)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["youtube"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":5,"maxLength":20}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeVideoDetail"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get comments on a YouTube video

> Returns top-level comments for a video. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive). \`sort\` is \`top\` (default) or \`latest\`.\
> \
> \*\*Price:\*\* $0.0003 per item\
> \
> \*\*Minimum charge:\*\* $0.003 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"youtube","description":"YouTube: channels, videos, shorts, live streams, comments, search, trending, hashtags."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"YoutubeComment":{"type":"object","properties":{"type":{"type":"string"},"id":{"type":"string"},"text":{"type":"string"},"likeCount":{"type":"string"},"replyCount":{"type":"string"},"publishedTime":{"type":"string"},"author":{"$ref":"#/components/schemas/YoutubeCommentAuthor"}}},"YoutubeCommentAuthor":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"thumbnails":{"type":"string","format":"uri"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/youtube/comments":{"get":{"operationId":"getYoutubeComments","summary":"Get comments on a YouTube video","description":"Returns top-level comments for a video. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive). `sort` is `top` (default) or `latest`.\n\n**Price:** $0.0003 per item\n\n**Minimum charge:** $0.003 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["youtube"],"parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string","minLength":5,"maxLength":20}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["top","latest"]}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeComment"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Search YouTube

> Search YouTube. \`mode\` selects what to search for (\`video\`, \`channel\`, or \`playlist\`). When \`mode=video\` results match \`YoutubeVideo\`; other modes return shape variants. Use \`cursor\` to paginate or \`maxItems\` for a one-shot bulk pull (mutually exclusive).\
> \
> \*\*Price:\*\* $0.0003 per item\
> \
> \*\*Minimum charge:\*\* $0.003 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"youtube","description":"YouTube: channels, videos, shorts, live streams, comments, search, trending, hashtags."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"YoutubeVideo":{"type":"object","description":"Video result from search, hashtag, or trending endpoints.","properties":{"type":{"type":"string"},"id":{"type":"string"},"url":{"type":"string","format":"uri"},"title":{"type":"string"},"description":{"type":"string"},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}},"richThumbnail":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}},"channel":{"$ref":"#/components/schemas/YoutubeChannelRef"},"publishDate":{"type":"string"},"views":{"type":"string"},"duration":{"type":"string"}}},"YoutubeThumbnail":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"width":{"type":"integer"},"height":{"type":"integer"}}},"YoutubeChannelRef":{"type":"object","description":"Lightweight channel reference embedded in video or comment results.","properties":{"id":{"type":"string"},"name":{"type":"string"},"handle":{"type":"string"},"url":{"type":"string","format":"uri"},"subscriberCount":{"type":"string"},"badges":{"type":"array","items":{"type":"string"}},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/youtube/search":{"get":{"operationId":"searchYoutube","summary":"Search YouTube","description":"Search YouTube. `mode` selects what to search for (`video`, `channel`, or `playlist`). When `mode=video` results match `YoutubeVideo`; other modes return shape variants. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).\n\n**Price:** $0.0003 per item\n\n**Minimum charge:** $0.003 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["youtube"],"parameters":[{"name":"query","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"mode","in":"query","required":true,"schema":{"type":"string","enum":["video","channel","playlist"]}},{"name":"sortBy","in":"query","required":false,"schema":{"type":"string","enum":["relevance","rating","upload_date","view_count"]}},{"name":"lang","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":5}},{"name":"geo","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":5}},{"name":"duration","in":"query","required":false,"schema":{"type":"string","enum":["short","medium","long"]}},{"name":"features","in":"query","required":false,"schema":{"type":"string","enum":["360","hd","subtitles","creative_commons","3d","live","purchased","4k","location","hdr","vr180"]}},{"name":"uploadDate","in":"query","required":false,"schema":{"type":"string","enum":["hour","today","week","month","year"]}},{"name":"maxItems","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeVideo"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get videos under a YouTube hashtag

> Returns videos tagged with \`tag\` from a single request.\
> \
> \*\*Price:\*\* $0.0003 per item\
> \
> \*\*Minimum charge:\*\* $0.003 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"youtube","description":"YouTube: channels, videos, shorts, live streams, comments, search, trending, hashtags."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"YoutubeVideo":{"type":"object","description":"Video result from search, hashtag, or trending endpoints.","properties":{"type":{"type":"string"},"id":{"type":"string"},"url":{"type":"string","format":"uri"},"title":{"type":"string"},"description":{"type":"string"},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}},"richThumbnail":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}},"channel":{"$ref":"#/components/schemas/YoutubeChannelRef"},"publishDate":{"type":"string"},"views":{"type":"string"},"duration":{"type":"string"}}},"YoutubeThumbnail":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"width":{"type":"integer"},"height":{"type":"integer"}}},"YoutubeChannelRef":{"type":"object","description":"Lightweight channel reference embedded in video or comment results.","properties":{"id":{"type":"string"},"name":{"type":"string"},"handle":{"type":"string"},"url":{"type":"string","format":"uri"},"subscriberCount":{"type":"string"},"badges":{"type":"array","items":{"type":"string"}},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/youtube/hashtag":{"get":{"operationId":"getYoutubeHashtag","summary":"Get videos under a YouTube hashtag","description":"Returns videos tagged with `tag` from a single request.\n\n**Price:** $0.0003 per item\n\n**Minimum charge:** $0.003 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["youtube"],"parameters":[{"name":"tag","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeVideo"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get trending YouTube videos

> Returns currently trending videos. Use \`geo\` to target a country (e.g. \`US\`, \`GB\`).\
> \
> \*\*Price:\*\* $0.0003 per item\
> \
> \*\*Minimum charge:\*\* $0.003 per request (10 items)\
> \
> Your current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"tags":[{"name":"youtube","description":"YouTube: channels, videos, shorts, live streams, comments, search, trending, hashtags."}],"servers":[{"url":"https://api.pullbay.com"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"YoutubeVideo":{"type":"object","description":"Video result from search, hashtag, or trending endpoints.","properties":{"type":{"type":"string"},"id":{"type":"string"},"url":{"type":"string","format":"uri"},"title":{"type":"string"},"description":{"type":"string"},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}},"richThumbnail":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}},"channel":{"$ref":"#/components/schemas/YoutubeChannelRef"},"publishDate":{"type":"string"},"views":{"type":"string"},"duration":{"type":"string"}}},"YoutubeThumbnail":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"width":{"type":"integer"},"height":{"type":"integer"}}},"YoutubeChannelRef":{"type":"object","description":"Lightweight channel reference embedded in video or comment results.","properties":{"id":{"type":"string"},"name":{"type":"string"},"handle":{"type":"string"},"url":{"type":"string","format":"uri"},"subscriberCount":{"type":"string"},"badges":{"type":"array","items":{"type":"string"}},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}}}},"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}},"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}},"paths":{"/youtube/trending":{"get":{"operationId":"getYoutubeTrending","summary":"Get trending YouTube videos","description":"Returns currently trending videos. Use `geo` to target a country (e.g. `US`, `GB`).\n\n**Price:** $0.0003 per item\n\n**Minimum charge:** $0.003 per request (10 items)\n\nYour current plan discount applies to the total. The minimum applies once per request, even when fewer items are returned. maxItems limits requested results; it does not lower the minimum billable count.","tags":["youtube"],"parameters":[{"name":"geo","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":5}}],"responses":{"200":{"description":"Successful request","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeVideo"}},"pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount, rounded once to a microdollar. This example uses the minimum count with no discount."},"billableItems":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```


# Models

## The Pagination object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"Pagination":{"type":"object","properties":{"page":{"type":"integer","nullable":true},"hasNextPage":{"type":"boolean","nullable":true},"cursor":{"type":"string","nullable":true},"offset":{"type":"string","nullable":true,"description":"Numeric offset string for offset-paginated endpoints (e.g. Google Maps place search)."}}}}}}
```

## The Pricing object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"Pricing":{"type":"object","required":["currency","amountCharged","billableItems"],"properties":{"currency":{"type":"string","enum":["USD"]},"amountCharged":{"type":"string","pattern":"^\\d+\\.\\d{6}$","description":"Exact USD amount after the current plan discount."},"billableItems":{"type":"integer","minimum":0}}}}}}
```

## The ErrorEnvelope object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"ErrorEnvelope":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer"},"message":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object","properties":{"code":{"type":"string"}}}}}}}}
```

## The InsufficientBalanceError object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"InsufficientBalanceError":{"type":"object","description":"The available USD balance cannot cover this request. No partial charge is retained. Add funds or reduce the requested result count, subject to the endpoint minimum.","required":["status","success","error"],"properties":{"requestId":{"type":"string","format":"uuid"},"status":{"type":"integer","enum":[402]},"success":{"type":"boolean","enum":[false]},"message":{"type":"string"},"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["TEAM_INSUFFICIENT_BALANCE"]}}}}}}}}
```

## The ApartmentsProperty object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"ApartmentsProperty":{"type":"object","description":"An Apartments.com property summary (search result entry).","properties":{"id":{"type":"string"},"name":{"type":"string"},"bedRange":{"type":"string"},"rentRange":{"type":"string"},"address":{"type":"object","additionalProperties":true}}}}}}
```

## The ApartmentsPropertyDetail object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"ApartmentsPropertyDetail":{"type":"object","description":"Full property detail returned by /properties/{propertyId}.","properties":{"id":{"type":"string"},"propertyName":{"type":"string"},"url":{"type":"string"},"propertyWebsite":{"type":"string"},"lastUpdated":{"type":"string"},"description":{"type":"string"},"rating":{"type":"number"},"beds":{"type":"string"},"baths":{"type":"string"},"sqft":{"type":"string"},"baseRent":{"type":"object","additionalProperties":true},"totalRent":{"type":"object","additionalProperties":true},"location":{"type":"object","additionalProperties":true},"coordinates":{"type":"object","properties":{"latitude":{"type":"number"},"longitude":{"type":"number"}}},"contact":{"type":"object","additionalProperties":true},"specials":{"type":"object","nullable":true,"additionalProperties":true},"schools":{"type":"object","additionalProperties":true},"amenities":{"type":"array","items":{"type":"object","additionalProperties":true}},"fees":{"type":"array","items":{"type":"object","additionalProperties":true}},"models":{"type":"array","items":{"type":"object","additionalProperties":true}},"rentals":{"type":"array","items":{"type":"object","additionalProperties":true}},"transportation":{"type":"array","items":{"type":"object","additionalProperties":true}},"transitAndPOI":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}}
```

## The ApartmentsPropertyImages object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"ApartmentsPropertyImages":{"type":"object","description":"Photos, floor plans, and virtual tours for a property.","properties":{"photos":{"type":"array","items":{"type":"object","additionalProperties":true}},"floorPlans":{"type":"array","items":{"type":"object","additionalProperties":true}},"virtualTours":{"type":"array","items":{"type":"object","additionalProperties":true}},"virtualTourExtended":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}}
```

## The ApartmentsPropertyReview object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"ApartmentsPropertyReview":{"type":"object","description":"A review for an Apartments.com property.","properties":{"key":{"type":"string"},"title":{"type":"string"},"text":{"type":"string"},"rating":{"type":"number"},"submissionDate":{"type":"string"},"isOwner":{"type":"boolean"},"positiveFeedbackCount":{"type":"integer"},"comments":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}}
```

## The Application object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"Application":{"type":"object","properties":{"id":{"type":"integer"},"appId":{"type":"string"},"title":{"type":"string"},"url":{"type":"string","format":"uri"},"description":{"type":"string"},"icon":{"type":"string","format":"uri"},"genres":{"type":"array","items":{"type":"string"}},"genreIds":{"type":"array","items":{"type":"string"}},"primaryGenre":{"type":"string"},"primaryGenreId":{"type":"integer"},"contentRating":{"type":"string"},"languages":{"type":"array","items":{"type":"string"}},"size":{"type":"string"},"requiredOsVersion":{"type":"string"},"released":{"type":"string","format":"date-time"},"updated":{"type":"string","format":"date-time"},"releaseNotes":{"type":"string"},"version":{"type":"string"},"price":{"type":"number"},"currency":{"type":"string"},"free":{"type":"boolean"},"developerId":{"type":"integer"},"developer":{"type":"string"},"developerUrl":{"type":"string","format":"uri"},"developerWebsite":{"type":"string","format":"uri"},"score":{"type":"number"},"reviews":{"type":"integer"},"currentVersionScore":{"type":"number"},"currentVersionReviews":{"type":"integer"},"screenshots":{"type":"array","items":{"type":"string","format":"uri"}},"ipadScreenshots":{"type":"array","items":{"type":"string","format":"uri"}},"appletvScreenshots":{"type":"array","items":{"type":"string","format":"uri"}},"supportedDevices":{"type":"array","items":{"type":"string"}}}}}}}
```

## The Review object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"Review":{"type":"object","properties":{"id":{"type":"string"},"date":{"type":"string","format":"date-time"},"userName":{"type":"string"},"userUrl":{"type":"string","format":"uri"},"version":{"type":"string"},"score":{"type":"integer","minimum":1,"maximum":5},"title":{"type":"string"},"text":{"type":"string"},"url":{"type":"string","format":"uri"},"country":{"type":"string"}}}}}}
```

## The DuckDuckGoSearchResult object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"DuckDuckGoSearchResult":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"hostname":{"type":"string"},"icon":{"type":"string","format":"uri"},"url":{"type":"string","format":"uri"}}}}}}
```

## The DuckDuckGoImage object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"DuckDuckGoImage":{"type":"object","properties":{"title":{"type":"string"},"image":{"type":"string","format":"uri"},"thumbnail":{"type":"string","format":"uri"},"image_token":{"type":"string"},"thumbnail_token":{"type":"string"},"width":{"type":"integer"},"height":{"type":"integer"},"source":{"type":"string"},"url":{"type":"string","format":"uri"}}}}}}
```

## The DuckDuckGoNewsArticle object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"DuckDuckGoNewsArticle":{"type":"object","properties":{"title":{"type":"string"},"excerpt":{"type":"string"},"url":{"type":"string","format":"uri"},"image":{"type":"string","format":"uri"},"date":{"type":"integer","description":"Unix timestamp in seconds"},"relativeTime":{"type":"string"},"syndicate":{"type":"string"},"isOld":{"type":"boolean"}}}}}}
```

## The DuckDuckGoVideo object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"DuckDuckGoVideo":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"url":{"type":"string","format":"uri"},"image":{"type":"string","format":"uri"},"duration":{"type":"string"},"publishedOn":{"type":"string"},"published":{"type":"string","format":"date-time"},"publisher":{"type":"string"},"viewCount":{"type":"integer"}}}}}}
```

## The GoogleMapsLocation object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"GoogleMapsLocation":{"type":"object","description":"Latitude/longitude pair.","properties":{"lat":{"type":"number","format":"float"},"lng":{"type":"number","format":"float"}}}}}}
```

## The GoogleMapsPlaceSearchResult object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"GoogleMapsPlaceSearchResult":{"type":"object","description":"Place result returned by /google-maps/place/search.","properties":{"url":{"type":"string","format":"uri"},"title":{"type":"string"},"description":{"type":"string","nullable":true},"address":{"type":"string"},"neighborhood":{"type":"string","nullable":true},"street":{"type":"string","nullable":true},"district":{"type":"string","nullable":true},"postalCode":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"country":{"type":"string","nullable":true},"location":{"$ref":"#/components/schemas/GoogleMapsLocation"},"website":{"type":"string","format":"uri","nullable":true},"phone":{"type":"string","nullable":true},"price":{"type":"string","nullable":true},"totalScore":{"type":"number","format":"float"},"placeId":{"type":"string"},"fid":{"type":"string","description":"Google Maps feature ID, of the form '0x...:0x...'."},"reviewsCount":{"type":"integer"},"categories":{"type":"array","items":{"type":"string"}},"openingHours":{"type":"array","description":"Opening hours per weekday.","items":{"type":"object","additionalProperties":true}},"additionalInfo":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"type":"boolean"}},"description":"Grouped feature flags (e.g. Service options, Accessibility, Offerings)."}}},"GoogleMapsLocation":{"type":"object","description":"Latitude/longitude pair.","properties":{"lat":{"type":"number","format":"float"},"lng":{"type":"number","format":"float"}}}}}}
```

## The GoogleMapsReviewsDistribution object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"GoogleMapsReviewsDistribution":{"type":"object","properties":{"oneStar":{"type":"integer"},"twoStar":{"type":"integer"},"threeStar":{"type":"integer"},"fourStar":{"type":"integer"},"fiveStar":{"type":"integer"}}}}}}
```

## The GoogleMapsPlace object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"GoogleMapsPlace":{"type":"object","description":"Full place detail returned by /google-maps/place/{fid}.","properties":{"url":{"type":"string","format":"uri"},"title":{"type":"string"},"address":{"type":"string"},"neighborhood":{"type":"string","nullable":true},"street":{"type":"string","nullable":true},"district":{"type":"string","nullable":true},"postalCode":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"location":{"$ref":"#/components/schemas/GoogleMapsLocation"},"website":{"type":"string","format":"uri","nullable":true},"phone":{"type":"string","nullable":true},"phoneUnformatted":{"type":"string","nullable":true},"plusCode":{"type":"string","nullable":true},"price":{"type":"string","nullable":true},"totalScore":{"type":"number","format":"float"},"placeId":{"type":"string"},"cid":{"type":"string"},"fid":{"type":"string"},"reviewsCount":{"type":"integer"},"reviewsDistribution":{"$ref":"#/components/schemas/GoogleMapsReviewsDistribution"},"categories":{"type":"array","items":{"type":"string"}},"additionalInfo":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"type":"boolean"}},"description":"Grouped feature flags (e.g. Service options, Accessibility, Amenities)."}}},"GoogleMapsLocation":{"type":"object","description":"Latitude/longitude pair.","properties":{"lat":{"type":"number","format":"float"},"lng":{"type":"number","format":"float"}}},"GoogleMapsReviewsDistribution":{"type":"object","properties":{"oneStar":{"type":"integer"},"twoStar":{"type":"integer"},"threeStar":{"type":"integer"},"fourStar":{"type":"integer"},"fiveStar":{"type":"integer"}}}}}}
```

## The GoogleMapsReviewer object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"GoogleMapsReviewer":{"type":"object","properties":{"name":{"type":"string"},"photo":{"type":"string","format":"uri"},"url":{"type":"string","format":"uri"},"totalReviews":{"type":"integer"},"totalPhotos":{"type":"integer"}}}}}}
```

## The GoogleMapsPlaceReview object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"GoogleMapsPlaceReview":{"type":"object","properties":{"id":{"type":"string"},"link":{"type":"string","format":"uri"},"text":{"type":"string"},"stars":{"type":"integer","minimum":1,"maximum":5},"date":{"type":"integer","description":"Unix timestamp in milliseconds"},"dateIso":{"type":"string","format":"date-time"},"likes":{"type":"integer","nullable":true},"photos":{"type":"array","items":{"type":"string"}},"reviewer":{"$ref":"#/components/schemas/GoogleMapsReviewer"},"ownersResponse":{"type":"object","additionalProperties":true,"description":"Owner's response to the review (empty object when none)."},"details":{"type":"object","additionalProperties":{"type":"string"},"description":"Free-form key/value pairs of review metadata (e.g. \"Visited on\", \"Wait time\")."}}},"GoogleMapsReviewer":{"type":"object","properties":{"name":{"type":"string"},"photo":{"type":"string","format":"uri"},"url":{"type":"string","format":"uri"},"totalReviews":{"type":"integer"},"totalPhotos":{"type":"integer"}}}}}}
```

## The GoogleNewsArticle object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"GoogleNewsArticle":{"type":"object","properties":{"title":{"type":"string"},"articleUrl":{"type":"string","format":"uri"},"publishedAt":{"type":"string","format":"date-time"},"imageUrl":{"type":"string","format":"uri-reference","nullable":true},"source":{"type":"string"},"sourceIconUrl":{"type":"string","format":"uri"},"author":{"type":"string","nullable":true}}}}}}
```

## The GooglePlayApplication object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"GooglePlayApplication":{"type":"object","properties":{"title":{"type":"string"},"appId":{"type":"string"},"url":{"type":"string","format":"uri-reference"},"icon":{"type":"string","format":"uri"},"headerImage":{"type":"string","format":"uri"},"summary":{"type":"string"},"description":{"type":"string"},"descriptionHTML":{"type":"string"},"score":{"type":"number"},"scoreText":{"type":"string"},"ratings":{"type":"integer"},"reviews":{"type":"integer"},"histogram":{"type":"object","additionalProperties":{"type":"integer"},"description":"Rating histogram, keyed by star count (1-5)."},"free":{"type":"boolean"},"price":{"type":"number"},"priceText":{"type":"string"},"currency":{"type":"string"},"offersIAP":{"type":"boolean"},"adSupported":{"type":"boolean"},"installs":{"type":"string"},"minInstalls":{"type":"integer"},"maxInstalls":{"type":"integer"},"androidVersion":{"type":"string"},"androidVersionText":{"type":"string"},"updated":{"type":"integer","description":"Unix timestamp in milliseconds"},"genre":{"type":"string"},"genreId":{"type":"string"},"developer":{"type":"string"},"developerId":{"type":"string"},"developerEmail":{"type":"string"},"developerWebsite":{"type":"string","format":"uri"},"developerInternalID":{"type":"string"},"privacyPolicy":{"type":"string","format":"uri"},"playstoreUrl":{"type":"string","format":"uri"},"screenshots":{"type":"array","items":{"type":"string"}}}}}}}
```

## The GooglePlayApplicationSummary object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"GooglePlayApplicationSummary":{"type":"object","description":"Lightweight app reference returned by search and similar endpoints.","properties":{"title":{"type":"string"},"appId":{"type":"string"},"url":{"type":"string","format":"uri-reference"},"icon":{"type":"string","format":"uri"},"summary":{"type":"string"},"score":{"type":"number"},"scoreText":{"type":"string"},"free":{"type":"boolean"},"price":{"type":"number"},"currency":{"type":"string"},"playstoreUrl":{"type":"string","format":"uri"},"developer":{"type":"string"},"developerId":{"type":"string"}}}}}}
```

## The GooglePlayReviewCriteria object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"GooglePlayReviewCriteria":{"type":"object","properties":{"criteria":{"type":"string"},"rating":{"type":"integer"}}}}}}
```

## The GooglePlayReview object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"GooglePlayReview":{"type":"object","properties":{"id":{"type":"string"},"userName":{"type":"string"},"userImage":{"type":"string","format":"uri"},"date":{"type":"string","format":"date-time"},"score":{"type":"integer","minimum":1,"maximum":5},"scoreText":{"type":"string"},"url":{"type":"string","format":"uri"},"title":{"type":"string","nullable":true},"text":{"type":"string"},"replyDate":{"type":"string","format":"date-time","nullable":true},"replyText":{"type":"string","nullable":true},"version":{"type":"string"},"thumbsUp":{"type":"integer"},"country":{"type":"string"},"criterias":{"type":"array","items":{"$ref":"#/components/schemas/GooglePlayReviewCriteria"}}}},"GooglePlayReviewCriteria":{"type":"object","properties":{"criteria":{"type":"string"},"rating":{"type":"integer"}}}}}}
```

## The GooglePlayPermission object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"GooglePlayPermission":{"type":"object","properties":{"permission":{"type":"string"},"type":{"type":"string"}}}}}}
```

## The GooglePlayDataSafetyItem object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"GooglePlayDataSafetyItem":{"type":"object","properties":{"data":{"type":"string"},"purpose":{"type":"string"},"type":{"type":"string"},"optional":{"type":"boolean"}}}}}}
```

## The GooglePlaySecurityPractice object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"GooglePlaySecurityPractice":{"type":"object","properties":{"practice":{"type":"string"},"description":{"type":"string"}}}}}}
```

## The GooglePlayDataSafety object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"GooglePlayDataSafety":{"type":"object","properties":{"sharedData":{"type":"array","items":{"$ref":"#/components/schemas/GooglePlayDataSafetyItem"}},"collectedData":{"type":"array","items":{"$ref":"#/components/schemas/GooglePlayDataSafetyItem"}},"securityPractices":{"type":"array","items":{"$ref":"#/components/schemas/GooglePlaySecurityPractice"}},"privacyPolicyUrl":{"type":"string","format":"uri"}}},"GooglePlayDataSafetyItem":{"type":"object","properties":{"data":{"type":"string"},"purpose":{"type":"string"},"type":{"type":"string"},"optional":{"type":"boolean"}}},"GooglePlaySecurityPractice":{"type":"object","properties":{"practice":{"type":"string"},"description":{"type":"string"}}}}}}
```

## The GoogleSearchResult object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"GoogleSearchResult":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"link":{"type":"string","format":"uri"},"snippet":{"type":"string"}}}}}}
```

## The RedditAuthor object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"RedditAuthor":{"type":"object","description":"Lightweight author reference embedded in posts and comments.","properties":{"id":{"type":"string"},"username":{"type":"string"},"isBlocked":{"type":"boolean"},"isDeleted":{"type":"boolean"}}}}}}
```

## The RedditSubredditRef object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"RedditSubredditRef":{"type":"object","description":"Lightweight subreddit reference embedded in posts and comments.","properties":{"id":{"type":"string"},"name":{"type":"string"},"prefixedName":{"type":"string"},"type":{"type":"string"}}}}}}
```

## The RedditPost object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"RedditPost":{"type":"object","description":"A Reddit post (link or self-text).","properties":{"type":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"text":{"type":"string"},"textHTML":{"type":"string"},"url":{"type":"string"},"created":{"type":"integer","description":"Unix timestamp in seconds."},"editedAt":{"oneOf":[{"type":"integer"},{"type":"boolean"}]},"score":{"type":"integer"},"upvotes":{"type":"integer"},"downvotes":{"type":"integer"},"upvoteRatio":{"type":"number"},"numComments":{"type":"integer"},"viewCount":{"type":"integer","nullable":true},"goldCount":{"type":"integer"},"numCrossposts":{"type":"integer"},"isLocked":{"type":"boolean"},"isStickied":{"type":"boolean"},"isArchived":{"type":"boolean"},"isOriginalContent":{"type":"boolean"},"isCrosspostable":{"type":"boolean"},"isNSFW":{"type":"boolean"},"isSpoiler":{"type":"boolean"},"isMediaOnly":{"type":"boolean"},"isSponsored":{"type":"boolean"},"isMeta":{"type":"boolean"},"isGildable":{"type":"boolean"},"isScoreHidden":{"type":"boolean"},"contestMode":{"type":"boolean"},"hidden":{"type":"boolean"},"sendReplies":{"type":"boolean"},"author":{"$ref":"#/components/schemas/RedditAuthor"},"subreddit":{"$ref":"#/components/schemas/RedditSubredditRef"},"flair":{"type":"object","nullable":true},"thumbnail":{"type":"object","additionalProperties":true},"preview":{"type":"object","additionalProperties":true},"awards":{"type":"array","items":{"type":"object","additionalProperties":true}},"flairs":{"type":"array","items":{"type":"object","additionalProperties":true}},"gallery":{"type":"array","items":{"type":"object","additionalProperties":true}},"crosspostParents":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"RedditAuthor":{"type":"object","description":"Lightweight author reference embedded in posts and comments.","properties":{"id":{"type":"string"},"username":{"type":"string"},"isBlocked":{"type":"boolean"},"isDeleted":{"type":"boolean"}}},"RedditSubredditRef":{"type":"object","description":"Lightweight subreddit reference embedded in posts and comments.","properties":{"id":{"type":"string"},"name":{"type":"string"},"prefixedName":{"type":"string"},"type":{"type":"string"}}}}}}
```

## The RedditComment object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"RedditComment":{"type":"object","description":"A Reddit comment.","properties":{"type":{"type":"string"},"id":{"type":"string"},"postId":{"type":"string"},"parentId":{"type":"string"},"body":{"type":"string"},"bodyHTML":{"type":"string"},"url":{"type":"string"},"createdAt":{"type":"integer","description":"Unix timestamp in seconds."},"editedAt":{"oneOf":[{"type":"integer"},{"type":"boolean"}]},"score":{"type":"integer"},"upvotes":{"type":"integer"},"downvotes":{"type":"integer"},"goldCount":{"type":"integer"},"totalAwardsReceived":{"type":"integer"},"controversiality":{"type":"integer"},"isLocked":{"type":"boolean"},"isStickied":{"type":"boolean"},"isArchived":{"type":"boolean"},"isSaved":{"type":"boolean"},"isScoreHidden":{"type":"boolean"},"isCollapsed":{"type":"boolean"},"noFollow":{"type":"boolean"},"sendReplies":{"type":"boolean"},"hasReplies":{"type":"boolean"},"author":{"$ref":"#/components/schemas/RedditAuthor"},"subreddit":{"$ref":"#/components/schemas/RedditSubredditRef"},"awards":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"RedditAuthor":{"type":"object","description":"Lightweight author reference embedded in posts and comments.","properties":{"id":{"type":"string"},"username":{"type":"string"},"isBlocked":{"type":"boolean"},"isDeleted":{"type":"boolean"}}},"RedditSubredditRef":{"type":"object","description":"Lightweight subreddit reference embedded in posts and comments.","properties":{"id":{"type":"string"},"name":{"type":"string"},"prefixedName":{"type":"string"},"type":{"type":"string"}}}}}}
```

## The RedditUser object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"RedditUser":{"type":"object","description":"A Reddit user profile.","properties":{"type":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"createdAt":{"type":"integer","description":"Unix timestamp in seconds."},"iconImage":{"type":"string"},"snoovatarImage":{"type":"string"},"bannerImage":{"type":"string"},"awarderKarma":{"type":"integer"},"postKarma":{"type":"integer"},"commentKarma":{"type":"integer"},"totalKarma":{"type":"integer"},"hasVerifiedEmail":{"type":"boolean"},"over18":{"type":"boolean"}}}}}}
```

## The RedditSubreddit object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"RedditSubreddit":{"type":"object","description":"A subreddit profile.","properties":{"type":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"prefixedName":{"type":"string"},"title":{"type":"string"},"subType":{"type":"string"},"description":{"type":"string"},"descriptionHtml":{"type":"string"},"publicDescription":{"type":"string"},"publicDescriptionHtml":{"type":"string"},"submitText":{"type":"string"},"submitTextHtml":{"type":"string","nullable":true},"submissionType":{"type":"string"},"advertiserCategory":{"type":"string"},"language":{"type":"string"},"url":{"type":"string"},"headerTitle":{"type":"string"},"iconImage":{"type":"string"},"iconSize":{"type":"array","nullable":true,"items":{"type":"integer"}},"bannerImage":{"type":"string"},"bannerBackgroundImage":{"type":"string"},"bannerSize":{"type":"array","nullable":true,"items":{"type":"integer"}},"headerImage":{"type":"string"},"headerSize":{"type":"array","items":{"type":"integer"}},"primaryColor":{"type":"string"},"keyColor":{"type":"string"},"createdAt":{"type":"integer","description":"Unix timestamp in seconds."},"subscribers":{"type":"integer"},"commentScoreHideMins":{"type":"integer"},"notificationLevel":{"type":"string","nullable":true},"suggestedCommentSort":{"type":"string","nullable":true},"userFlairEnabled":{"type":"boolean"},"linkFlairEnabled":{"type":"boolean"},"wikiEnabled":{"type":"boolean"},"emojisEnabled":{"type":"boolean"},"spoilersEnabled":{"type":"boolean"},"allowImages":{"type":"boolean"},"allowVideos":{"type":"boolean"},"allowGalleries":{"type":"boolean"},"allowPolls":{"type":"boolean"},"allowPredictions":{"type":"boolean"},"allowTalks":{"type":"boolean"},"allowDiscovery":{"type":"boolean"},"isCrosspostable":{"type":"boolean"},"publicTraffic":{"type":"boolean"},"showMediaPreview":{"type":"boolean"},"showMedia":{"type":"boolean"},"collapseDeletedComments":{"type":"boolean"},"shouldArchivePosts":{"type":"boolean"},"canAssignUserFlair":{"type":"boolean"},"canAssignLinkFlair":{"type":"boolean"},"restrictCommenting":{"type":"boolean"},"restrictPosting":{"type":"boolean"},"communityReviewed":{"type":"boolean"},"flairPosition":{"type":"string"},"linkFlairPosition":{"type":"string"}}}}}}
```

## The InstagramUserIdLookup object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"InstagramUserIdLookup":{"type":"object","description":"Result of resolving an Instagram user ID from a handle.","properties":{"isAvailable":{"type":"boolean"},"handle":{"type":"string"},"id":{"type":"string","description":"Present only when `isAvailable` is true."}}}}}}
```

## The InstagramUser object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"InstagramUser":{"type":"object","description":"An Instagram user profile.","properties":{"type":{"type":"string"},"isAvailable":{"type":"boolean"},"id":{"type":"string"},"username":{"type":"string"},"fullName":{"type":"string"},"biography":{"type":"string"},"profilePicUrl":{"type":"string"},"website":{"type":"string"},"category":{"type":"string"},"cityName":{"type":"string","nullable":true},"isPrivate":{"type":"boolean"},"isVerified":{"type":"boolean"},"isBusiness":{"type":"boolean"},"hasVideos":{"type":"boolean"},"hasGuides":{"type":"boolean"},"postCount":{"type":"integer"},"followerCount":{"type":"integer"},"followingCount":{"type":"integer"},"totalReelsCount":{"type":"integer"},"bioLinks":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}}
```

## The InstagramPostOwner object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"InstagramPostOwner":{"type":"object","description":"Lightweight user reference embedded in posts and stories.","properties":{"id":{"type":"string"},"username":{"type":"string"},"fullName":{"type":"string"},"profilePicUrl":{"type":"string"},"isVerified":{"type":"boolean"},"isPrivate":{"type":"boolean"}}}}}}
```

## The InstagramPost object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"InstagramPost":{"type":"object","description":"An Instagram post (image, video, carousel, or reel).","properties":{"type":{"type":"string","description":"photo / video / carousel / reel"},"id":{"type":"string"},"code":{"type":"string"},"url":{"type":"string"},"caption":{"type":"string"},"createdAt":{"type":"integer","description":"Unix timestamp in seconds."},"likeCount":{"type":"integer"},"commentCount":{"type":"integer"},"isAvailable":{"type":"boolean"},"isCarousel":{"type":"boolean"},"isVideo":{"type":"boolean"},"isPinned":{"type":"boolean"},"isPaidPartnership":{"type":"boolean"},"isLikeAndViewCountsDisabled":{"type":"boolean"},"owner":{"$ref":"#/components/schemas/InstagramPostOwner"},"coowners":{"type":"array","items":{"$ref":"#/components/schemas/InstagramPostOwner"}},"location":{"type":"object","nullable":true,"additionalProperties":true},"image":{"type":"object","nullable":true,"additionalProperties":true},"video":{"type":"object","nullable":true,"additionalProperties":true},"audio":{"type":"object","nullable":true,"additionalProperties":true},"carouselMedia":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"InstagramPostOwner":{"type":"object","description":"Lightweight user reference embedded in posts and stories.","properties":{"id":{"type":"string"},"username":{"type":"string"},"fullName":{"type":"string"},"profilePicUrl":{"type":"string"},"isVerified":{"type":"boolean"},"isPrivate":{"type":"boolean"}}}}}}
```

## The InstagramComment object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"InstagramComment":{"type":"object","description":"A comment on an Instagram post.","properties":{"type":{"type":"string"},"id":{"type":"string"},"userId":{"type":"string"},"message":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"likeCount":{"type":"integer"},"replyCount":{"type":"integer"},"isRanked":{"type":"boolean"},"user":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"string"},"username":{"type":"string"},"fullName":{"type":"string"},"profilePicUrl":{"type":"string"},"isPrivate":{"type":"boolean"},"isVerified":{"type":"boolean"}}}}}}}}
```

## The InstagramStory object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"InstagramStory":{"type":"object","description":"A single Instagram story slide.","properties":{"type":{"type":"string","description":"photo / video"},"id":{"type":"string"},"code":{"type":"string"},"createdAt":{"type":"integer","description":"Unix timestamp in seconds."},"expiringAt":{"type":"integer","description":"Unix timestamp in seconds."},"duration":{"type":"number"},"hasAudio":{"type":"boolean"},"hasTranslation":{"type":"boolean"},"canReshare":{"type":"boolean"},"canReply":{"type":"boolean"},"isVideo":{"type":"boolean"},"owner":{"$ref":"#/components/schemas/InstagramPostOwner"},"coowners":{"type":"array","items":{"$ref":"#/components/schemas/InstagramPostOwner"}},"image":{"type":"object","nullable":true,"additionalProperties":true},"video":{"type":"object","nullable":true,"additionalProperties":true},"audio":{"type":"object","nullable":true,"additionalProperties":true},"link":{"type":"object","nullable":true,"additionalProperties":true},"comments":{"type":"array","items":{"type":"object","additionalProperties":true}},"mentions":{"type":"array","items":{"type":"object","additionalProperties":true}},"hashtags":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"InstagramPostOwner":{"type":"object","description":"Lightweight user reference embedded in posts and stories.","properties":{"id":{"type":"string"},"username":{"type":"string"},"fullName":{"type":"string"},"profilePicUrl":{"type":"string"},"isVerified":{"type":"boolean"},"isPrivate":{"type":"boolean"}}}}}}
```

## The TiktokUser object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"TiktokUser":{"type":"object","description":"A TikTok user profile.","properties":{"id":{"type":"string"},"username":{"type":"string"},"nickname":{"type":"string"},"url":{"type":"string"},"bio":{"type":"string"},"bioUrl":{"type":"string"},"avatar":{"type":"string"},"verified":{"type":"boolean"},"profileCategory":{"type":"string"},"followers":{"type":"integer"},"following":{"type":"integer"},"likes":{"type":"integer"},"videos":{"type":"integer"}}}}}}
```

## The TiktokPostChannel object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"TiktokPostChannel":{"type":"object","description":"Lightweight user reference embedded in posts (post's author / channel).","properties":{"id":{"type":"string"},"username":{"type":"string"},"name":{"type":"string"},"bio":{"type":"string"},"avatar":{"type":"string"}}}}}}
```

## The TiktokPost object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"TiktokPost":{"type":"object","description":"A TikTok post (video).","properties":{"id":{"type":"string"},"title":{"type":"string"},"postPage":{"type":"string"},"uploadedAt":{"type":"integer","description":"Unix timestamp in seconds."},"uploadedAtFormatted":{"type":"string","format":"date-time"},"views":{"type":"integer"},"likes":{"type":"integer"},"comments":{"type":"integer"},"shares":{"type":"integer"},"bookmarks":{"type":"integer"},"poi":{"type":"object","nullable":true},"channel":{"$ref":"#/components/schemas/TiktokPostChannel"},"collabInfo":{"type":"object","additionalProperties":true},"video":{"type":"object","additionalProperties":true},"song":{"type":"object","additionalProperties":true},"hashtags":{"type":"array","items":{"type":"object","additionalProperties":true}},"subtitleInformation":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"TiktokPostChannel":{"type":"object","description":"Lightweight user reference embedded in posts (post's author / channel).","properties":{"id":{"type":"string"},"username":{"type":"string"},"name":{"type":"string"},"bio":{"type":"string"},"avatar":{"type":"string"}}}}}}
```

## The TiktokComment object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"TiktokComment":{"type":"object","description":"A comment or reply on a TikTok post.","properties":{"id":{"type":"string"},"parentId":{"type":"string","description":"Present only on replies — references the parent comment ID."},"awemeId":{"type":"string","description":"ID of the post the comment was made on."},"text":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"likeCount":{"type":"integer"},"replyCount":{"type":"integer","description":"Present only on top-level comments."},"commentLanguage":{"type":"string"},"isAuthorLiked":{"type":"boolean"},"user":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"string"},"username":{"type":"string"},"nickname":{"type":"string"},"avatar":{"type":"string"},"verified":{"type":"boolean"}}}}}}}}
```

## The TiktokHashtag object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"TiktokHashtag":{"type":"object","description":"A TikTok hashtag profile.","properties":{"id":{"type":"string"},"name":{"type":"string"},"views":{"type":"integer"}}}}}}
```

## The TwitterUser object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"TwitterUser":{"type":"object","description":"A Twitter user profile.","properties":{"type":{"type":"string"},"id":{"type":"string"},"userName":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"twitterUrl":{"type":"string"},"description":{"type":"string"},"location":{"type":"string"},"profilePicture":{"type":"string"},"coverPicture":{"type":"string"},"isVerified":{"type":"boolean"},"isBlueVerified":{"type":"boolean","nullable":true},"protected":{"type":"boolean"},"canDm":{"type":"boolean"},"canMediaTag":{"type":"boolean"},"followers":{"type":"integer"},"following":{"type":"integer"},"fastFollowersCount":{"type":"integer"},"favouritesCount":{"type":"integer"},"listedCount":{"type":"integer"},"mediaCount":{"type":"integer"},"statusesCount":{"type":"integer"},"createdAt":{"type":"string"},"status":{"type":"string"}}}}}}
```

## The TwitterTweet object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"TwitterTweet":{"type":"object","description":"A single tweet.","properties":{"type":{"type":"string"},"id":{"type":"string"},"url":{"type":"string"},"twitterUrl":{"type":"string"},"text":{"type":"string"},"fullText":{"type":"string"},"source":{"type":"string"},"lang":{"type":"string"},"createdAt":{"type":"string"},"conversationId":{"type":"string"},"retweetCount":{"type":"integer"},"replyCount":{"type":"integer"},"likeCount":{"type":"integer"},"quoteCount":{"type":"integer"},"bookmarkCount":{"type":"integer"},"viewCount":{"type":"integer","nullable":true},"isReply":{"type":"boolean"},"isPinned":{"type":"boolean"},"isRetweet":{"type":"boolean"},"isQuote":{"type":"boolean"},"author":{"$ref":"#/components/schemas/TwitterUser"},"media":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"TwitterUser":{"type":"object","description":"A Twitter user profile.","properties":{"type":{"type":"string"},"id":{"type":"string"},"userName":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"twitterUrl":{"type":"string"},"description":{"type":"string"},"location":{"type":"string"},"profilePicture":{"type":"string"},"coverPicture":{"type":"string"},"isVerified":{"type":"boolean"},"isBlueVerified":{"type":"boolean","nullable":true},"protected":{"type":"boolean"},"canDm":{"type":"boolean"},"canMediaTag":{"type":"boolean"},"followers":{"type":"integer"},"following":{"type":"integer"},"fastFollowersCount":{"type":"integer"},"favouritesCount":{"type":"integer"},"listedCount":{"type":"integer"},"mediaCount":{"type":"integer"},"statusesCount":{"type":"integer"},"createdAt":{"type":"string"},"status":{"type":"string"}}}}}}
```

## The TwitterUserAbout object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"TwitterUserAbout":{"type":"object","description":"Extended profile / about details for a Twitter user.","properties":{"accountBasedIn":{"type":"string"},"locationAccurate":{"type":"boolean"},"profileImageShape":{"type":"string"},"avatarUrl":{"type":"string"},"usernameChangeCount":{"type":"string"},"accountCreatedAt":{"type":"string"},"accountSource":{"type":"string"},"verifiedSince":{"type":"string"}}}}}}
```

## The YelpPlace object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"YelpPlace":{"type":"object","description":"A Yelp place / business profile.","properties":{"id":{"type":"string"},"alias":{"type":"string"},"title":{"type":"string"},"website":{"type":"string"},"phoneNumber":{"type":"string"},"avgRating":{"type":"number"},"unroundedAvgRating":{"type":"number"},"reviewCount":{"type":"integer"},"priceRange":{"type":"string"},"isBusinessClosed":{"type":"boolean"},"primaryPhoto":{"type":"string"},"mediaCount":{"type":"integer"},"address":{"type":"object","additionalProperties":true},"coordinates":{"type":"object","properties":{"latitude":{"type":"number"},"longitude":{"type":"number"}}},"categories":{"type":"array","items":{"type":"object","additionalProperties":true}},"operationHours":{"type":"array","items":{"type":"object","additionalProperties":true}},"amenities":{"type":"array","items":{"type":"object","additionalProperties":true}},"about":{"type":"object","additionalProperties":true}}}}}}
```

## The YelpReview object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"YelpReview":{"type":"object","description":"A Yelp review for a place.","properties":{"id":{"type":"string"},"url":{"type":"string"},"rating":{"type":"number"},"text":{"type":"string"},"language":{"type":"string"},"date":{"type":"string","format":"date-time"},"images":{"type":"array","items":{"type":"string"}},"feedback":{"type":"object","additionalProperties":true},"reviewer":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"string"},"name":{"type":"string"},"profileUrl":{"type":"string"},"image":{"type":"string"}}}}}}}}
```

## The YelpSearchResult object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"YelpSearchResult":{"type":"object","description":"A Yelp search result entry (place summary).","properties":{"id":{"type":"string"},"alias":{"type":"string"},"name":{"type":"string"},"rating":{"type":"number"},"reviewCount":{"type":"integer"},"priceRange":{"type":"string"},"phone":{"type":"string","nullable":true},"website":{"type":"string","nullable":true},"coordinates":{"type":"object","properties":{"latitude":{"type":"number"},"longitude":{"type":"number"}}},"categories":{"type":"array","items":{"type":"object","additionalProperties":true}},"images":{"type":"array","items":{"type":"string"}}}}}}}
```

## The YoutubeThumbnail object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"YoutubeThumbnail":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"width":{"type":"integer"},"height":{"type":"integer"}}}}}}
```

## The YoutubeChannelRef object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"YoutubeChannelRef":{"type":"object","description":"Lightweight channel reference embedded in video or comment results.","properties":{"id":{"type":"string"},"name":{"type":"string"},"handle":{"type":"string"},"url":{"type":"string","format":"uri"},"subscriberCount":{"type":"string"},"badges":{"type":"array","items":{"type":"string"}},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}}}},"YoutubeThumbnail":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"width":{"type":"integer"},"height":{"type":"integer"}}}}}}
```

## The YoutubeChannel object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"YoutubeChannel":{"type":"object","properties":{"type":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"handle":{"type":"string"},"url":{"type":"string","format":"uri"},"description":{"type":"string"},"thumbnails":{"type":"string","format":"uri"},"isFamilySafe":{"type":"boolean"},"subscriberCount":{"type":"string"},"availableCountries":{"type":"array","items":{"type":"string"}},"tags":{"type":"array","items":{"type":"string"}},"joinedAt":{"type":"string"},"viewCount":{"type":"string"},"videoCount":{"type":"integer"}}}}}}
```

## The YoutubeVideo object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"YoutubeVideo":{"type":"object","description":"Video result from search, hashtag, or trending endpoints.","properties":{"type":{"type":"string"},"id":{"type":"string"},"url":{"type":"string","format":"uri"},"title":{"type":"string"},"description":{"type":"string"},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}},"richThumbnail":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}},"channel":{"$ref":"#/components/schemas/YoutubeChannelRef"},"publishDate":{"type":"string"},"views":{"type":"string"},"duration":{"type":"string"}}},"YoutubeThumbnail":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"width":{"type":"integer"},"height":{"type":"integer"}}},"YoutubeChannelRef":{"type":"object","description":"Lightweight channel reference embedded in video or comment results.","properties":{"id":{"type":"string"},"name":{"type":"string"},"handle":{"type":"string"},"url":{"type":"string","format":"uri"},"subscriberCount":{"type":"string"},"badges":{"type":"array","items":{"type":"string"}},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}}}}}}}
```

## The YoutubeVideoDetail object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"YoutubeVideoDetail":{"type":"object","description":"Full video metadata returned by /youtube/video.","properties":{"type":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"status":{"type":"string"},"url":{"type":"string","format":"uri"},"description":{"type":"string"},"duration":{"type":"integer","description":"Length in seconds."},"views":{"type":"integer"},"likes":{"type":"integer"},"comments":{"type":"integer"},"channel":{"$ref":"#/components/schemas/YoutubeChannelRef"},"keywords":{"type":"array","items":{"type":"string"}},"superTitle":{"type":"string"},"category":{"type":"string"},"isRatingsAllowed":{"type":"boolean"},"isLive":{"type":"boolean"},"isFamilySafe":{"type":"boolean"},"isPrivate":{"type":"boolean"},"isExclusive":{"type":"boolean"},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}}}},"YoutubeChannelRef":{"type":"object","description":"Lightweight channel reference embedded in video or comment results.","properties":{"id":{"type":"string"},"name":{"type":"string"},"handle":{"type":"string"},"url":{"type":"string","format":"uri"},"subscriberCount":{"type":"string"},"badges":{"type":"array","items":{"type":"string"}},"thumbnails":{"type":"array","items":{"$ref":"#/components/schemas/YoutubeThumbnail"}}}},"YoutubeThumbnail":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"width":{"type":"integer"},"height":{"type":"integer"}}}}}}
```

## The YoutubeCommentAuthor object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"YoutubeCommentAuthor":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"thumbnails":{"type":"string","format":"uri"}}}}}}
```

## The YoutubeComment object

```json
{"openapi":"3.0.3","info":{"title":"Pullbay API","version":"2.0.0"},"components":{"schemas":{"YoutubeComment":{"type":"object","properties":{"type":{"type":"string"},"id":{"type":"string"},"text":{"type":"string"},"likeCount":{"type":"string"},"replyCount":{"type":"string"},"publishedTime":{"type":"string"},"author":{"$ref":"#/components/schemas/YoutubeCommentAuthor"}}},"YoutubeCommentAuthor":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"thumbnails":{"type":"string","format":"uri"}}}}}}
```


# Apartments

Apartments.com: properties, photos, and reviews. All endpoints are read-only `GET` requests and return public Apartments.com data — no listing of your own is required.

**Base URL:** `https://api.pullbay.com/`

### Endpoints

| Endpoint                                          | Description                   | Pagination      |
| ------------------------------------------------- | ----------------------------- | --------------- |
| `GET /apartments/properties`                      | Search properties by location | Page / maxItems |
| `GET /apartments/properties/{propertyId}`         | Get a property by ID          | —               |
| `GET /apartments/properties/{propertyId}/images`  | Get images for a property     | —               |
| `GET /apartments/properties/{propertyId}/reviews` | Get reviews for a property    | Page / maxItems |

***

#### Search Properties

Search Apartments.com properties by location with optional rent/bed/bath filters.

**Request**

```
GET /apartments/properties
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Description                                                       |
| ---------- | ------- | -------- | ----------------------------------------------------------------- |
| `location` | string  | Yes      | Location to search, 2–40 characters (e.g. `Seattle, WA`)          |
| `sort`     | string  | No       | One of `default`, `lastUpdated`, `rentHighToLow`, `rentLowToHigh` |
| `minRent`  | number  | No       | Minimum monthly rent, 0–999999999                                 |
| `maxRent`  | number  | No       | Maximum monthly rent, 0–999999999                                 |
| `minBed`   | number  | No       | Minimum bedroom count, 0–4                                        |
| `maxBed`   | number  | No       | Maximum bedroom count, 0–4                                        |
| `minBath`  | number  | No       | Minimum bathroom count, 0–3                                       |
| `maxBath`  | number  | No       | Maximum bathroom count, 0–3                                       |
| `page`     | integer | No       | Page number, 1–100. Mutually exclusive with `maxItems`            |
| `maxItems` | integer | No       | One-shot bulk pull, 1–200 results. Mutually exclusive with `page` |

**Example**

bash

```bash
curl -G "https://api.pullbay.com/apartments/properties" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d location="Seattle, WA" \
  -d minRent=1500 \
  -d maxBed=2 \
  -d page=1
```

**Response**

json

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "swv1xf7",
      "name": "110 Roy Apartments",
      "bedRange": "Studio - 2 Beds",
      "rentRange": "$1,500 - $3,500",
      "address": {
        "streetAddress": "110 Roy St",
        "city": "Seattle",
        "state": "WA",
        "postalCode": "98109"
      }
    }
  ],
  "pagination": { "page": 1, "hasNextPage": true, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of properties returned, not the number of requests.

***

#### Get Property

Returns full details for an Apartments.com property. Use the `includeScore`, `includeModelDetails`, and `includeImages` flags to opt into extra fields.

**Request**

```
GET /apartments/properties/{propertyId}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter             | Type   | Required | Description                                                       |
| --------------------- | ------ | -------- | ----------------------------------------------------------------- |
| `propertyId`          | string | Yes      | Apartments.com property ID, 1–20 characters (path parameter)      |
| `includeScore`        | string | No       | `true` or `false` — include the property's rating in the response |
| `includeModelDetails` | string | No       | `true` or `false` — include detailed floor plan/model data        |
| `includeImages`       | string | No       | `true` or `false` — include photo/floor plan/tour data inline     |

**Example**

bash

```bash
curl -G "https://api.pullbay.com/apartments/properties/swv1xf7" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d includeScore=true \
  -d includeModelDetails=true
```

**Response**

json

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "swv1xf7",
      "propertyName": "110 Roy Apartments",
      "url": "https://www.apartments.com/110-roy-apartments-seattle-wa/swv1xf7/",
      "propertyWebsite": "https://www.110royapts.example/",
      "lastUpdated": "2026-04-30T10:00:00.000Z",
      "description": "Modern apartments in the heart of Queen Anne.",
      "rating": 4.2,
      "beds": "Studio - 2 Beds",
      "baths": "1 - 2 Baths",
      "sqft": "500 - 1,200 sq ft",
      "baseRent": { "min": 1500, "max": 3500 },
      "totalRent": { "min": 1580, "max": 3620 },
      "location": {
        "streetAddress": "110 Roy St",
        "city": "Seattle",
        "state": "WA",
        "postalCode": "98109"
      },
      "coordinates": { "latitude": 47.6262, "longitude": -122.3521 },
      "contact": { "phone": "(206) 555-0110", "officeHours": "Mon-Fri 9am-6pm" },
      "specials": null,
      "schools": { "elementary": "John Hay Elementary", "district": "Seattle Public Schools" },
      "amenities": [
        { "category": "Unique Amenities", "items": ["In Unit Washer & Dryer", "Rooftop Deck"] }
      ],
      "fees": [
        { "name": "Application Fee", "amount": "$45" }
      ],
      "models": [
        { "name": "Studio A", "beds": 0, "baths": 1, "sqft": 500 }
      ],
      "rentals": [
        { "unit": "204", "rent": 1750, "sqft": 540, "availability": "2026-07-01" }
      ],
      "transportation": [
        { "type": "Transit", "name": "Route 8", "distance": "0.1 mi" }
      ],
      "transitAndPOI": [
        { "type": "Grocery", "name": "QFC Queen Anne", "distance": "0.3 mi" }
      ]
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

#### Get Property Images

Returns photos, floor plans, virtual tours, and extended virtual tours for a property.

**Request**

```
GET /apartments/properties/{propertyId}/images
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter    | Type   | Required | Description                                                  |
| ------------ | ------ | -------- | ------------------------------------------------------------ |
| `propertyId` | string | Yes      | Apartments.com property ID, 1–20 characters (path parameter) |

**Example**

bash

```bash
curl -G "https://api.pullbay.com/apartments/properties/swv1xf7/images" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

json

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "photos": [
        { "url": "https://images1.apartments.com/i2/swv1xf7-1/110-roy-apartments-seattle-wa.jpg", "caption": "Building exterior" }
      ],
      "floorPlans": [
        { "name": "Studio A", "url": "https://images1.apartments.com/i2/swv1xf7-floorplan-1/studio-a.jpg" }
      ],
      "virtualTours": [
        { "name": "Studio A 3D Tour", "url": "https://my.matterport.com/show/?m=example1" }
      ],
      "virtualTourExtended": [
        { "name": "Full Property Walkthrough", "url": "https://my.matterport.com/show/?m=example2" }
      ]
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

#### Get Property Reviews

Returns reviews for an Apartments.com property.

**Request**

```
GET /apartments/properties/{propertyId}/reviews
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter    | Type    | Required | Description                                                       |
| ------------ | ------- | -------- | ----------------------------------------------------------------- |
| `propertyId` | string  | Yes      | Apartments.com property ID, 1–20 characters (path parameter)      |
| `page`       | integer | No       | Page number, 1–100. Mutually exclusive with `maxItems`            |
| `maxItems`   | integer | No       | One-shot bulk pull, 1–200 results. Mutually exclusive with `page` |

**Example**

bash

```bash
curl -G "https://api.pullbay.com/apartments/properties/swv1xf7/reviews" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d page=1
```

**Response**

json

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "key": "r-12345",
      "title": "Loved living here",
      "text": "Great staff and amenities. Highly recommend!",
      "rating": 4.5,
      "submissionDate": "Apr 30, 2026",
      "isOwner": false,
      "positiveFeedbackCount": 3,
      "comments": [
        { "author": "Property Manager", "text": "Thank you for the kind words!" }
      ]
    }
  ],
  "pagination": { "page": 1, "hasNextPage": true, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of reviews returned, not the number of requests.

***

### Object schemas

#### ApartmentsProperty object

| Field       | Type   | Description                                             |
| ----------- | ------ | ------------------------------------------------------- |
| `id`        | string | Apartments.com property ID                              |
| `name`      | string | Property name                                           |
| `bedRange`  | string | Bedroom range across all units (e.g. `Studio - 2 Beds`) |
| `rentRange` | string | Rent range across all units (e.g. `$1,500 - $3,500`)    |
| `address`   | object | Property address fields                                 |

#### ApartmentsPropertyDetail object

| Field             | Type               | Description                                                 |
| ----------------- | ------------------ | ----------------------------------------------------------- |
| `id`              | string             | Apartments.com property ID                                  |
| `propertyName`    | string             | Property name                                               |
| `url`             | string             | Apartments.com listing URL                                  |
| `propertyWebsite` | string             | Property's own website                                      |
| `lastUpdated`     | string (date-time) | When the listing was last updated                           |
| `description`     | string             | Property description                                        |
| `rating`          | number             | Average rating (present when `includeScore=true`)           |
| `beds`            | string             | Bedroom range                                               |
| `baths`           | string             | Bathroom range                                              |
| `sqft`            | string             | Square footage range                                        |
| `baseRent`        | object             | Base rent range                                             |
| `totalRent`       | object             | Total rent range (including estimated fees)                 |
| `location`        | object             | Address fields                                              |
| `coordinates`     | object             | `latitude` / `longitude`                                    |
| `contact`         | object             | Contact info (phone, office hours, etc.)                    |
| `specials`        | object, nullable   | Current leasing specials, if any                            |
| `schools`         | object             | Nearby school info                                          |
| `amenities`       | array              | Amenity categories and items                                |
| `fees`            | array              | Fee names and amounts                                       |
| `models`          | array              | Floor plan models (present when `includeModelDetails=true`) |
| `rentals`         | array              | Individual available units                                  |
| `transportation`  | array              | Nearby transit options                                      |
| `transitAndPOI`   | array              | Nearby transit and points of interest                       |

#### ApartmentsPropertyImages object

| Field                 | Type  | Description                      |
| --------------------- | ----- | -------------------------------- |
| `photos`              | array | Property photos                  |
| `floorPlans`          | array | Floor plan images                |
| `virtualTours`        | array | Virtual tour links               |
| `virtualTourExtended` | array | Extended/full virtual tour links |

#### ApartmentsPropertyReview object

| Field                   | Type    | Description                                             |
| ----------------------- | ------- | ------------------------------------------------------- |
| `key`                   | string  | Review ID                                               |
| `title`                 | string  | Review title                                            |
| `text`                  | string  | Review body                                             |
| `rating`                | number  | Rating, e.g. out of 5                                   |
| `submissionDate`        | string  | Date the review was submitted (e.g. `Apr 30, 2026`)     |
| `isOwner`               | boolean | Whether the reviewer is the property owner/manager      |
| `positiveFeedbackCount` | integer | Number of "helpful" votes                               |
| `comments`              | array   | Replies to the review (e.g. property manager responses) |

***

### Errors

All non-2xx responses share the same envelope:

json

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                  |
| ------ | ---------------------------------------- |
| `400`  | Bad request — missing/invalid parameters |
| `402`  | Insufficient credits                     |
| `404`  | Property or review set not found         |

### Finding Property IDs

* The property ID is the short alphanumeric slug at the end of an Apartments.com listing URL, e.g. `https://www.apartments.com/110-roy-apartments-seattle-wa/swv1xf7/` → `swv1xf7`.
* Run a `properties` search by location first to discover IDs, then use the ID with the detail, images, and reviews endpoints.

### Pagination Notes

* `properties` and `reviews` both use `page` (1-indexed) for page-based pagination, or `maxItems` for a one-shot bulk pull — the two are mutually exclusive on a single request.
* `pagination.hasNextPage` tells you whether to request the next page; `cursor` and `offset` are always `null` for this service since it is page-based, not cursor- or offset-based.


# DuckDuckGo

DuckDuckGo: web search, images, news, videos. All endpoints are read-only `GET` requests and return public DuckDuckGo search results.

**Base URL:** `https://api.pullbay.com/`

## Endpoints

| Endpoint                 | Description                         | Pagination |
| ------------------------ | ----------------------------------- | ---------- |
| `GET /duckduckgo/search` | Search the web via DuckDuckGo       | Offset     |
| `GET /duckduckgo/images` | Search images via DuckDuckGo        | Offset     |
| `GET /duckduckgo/news`   | Search news articles via DuckDuckGo | Offset     |
| `GET /duckduckgo/videos` | Search videos via DuckDuckGo        | Offset     |

***

### Search Web

Returns web search results for `term`.

**Request**

```bash
GET /duckduckgo/search
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter    | Type   | Required | Description                                                               |
| ------------ | ------ | -------- | ------------------------------------------------------------------------- |
| `term`       | string | Yes      | Search term, 1–255 characters                                             |
| `region`     | string | Yes      | DuckDuckGo region code (e.g. `wt-wt`, `us-en`, `uk-en`)                   |
| `safeSearch` | string | Yes      | `false` (off), `moderate`, or `strict`                                    |
| `offset`     | number | No       | Result offset, 0 or greater                                               |
| `time`       | string | No       | Recency filter: `a` (any), `d` (day), `w` (week), `m` (month), `y` (year) |

**Example**

```bash
curl -G "https://api.pullbay.com/duckduckgo/search" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d term="OpenAI" \
  -d region=us-en \
  -d safeSearch=moderate \
  -d offset=0
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "title": "OpenAI",
      "description": "We believe our research will eventually lead to artificial general intelligence, a system that can solve human-level problems.",
      "hostname": "openai.com",
      "icon": "https://external-content.duckduckgo.com/ip3/openai.com.ico",
      "url": "https://openai.com/"
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of results returned, not the number of requests.

***

### Search Images

Returns image search results for `term`.

**Request**

```bash
GET /duckduckgo/images
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter    | Type   | Required | Description                                             |
| ------------ | ------ | -------- | ------------------------------------------------------- |
| `term`       | string | Yes      | Search term, 1–255 characters                           |
| `region`     | string | Yes      | DuckDuckGo region code (e.g. `wt-wt`, `us-en`, `uk-en`) |
| `safeSearch` | string | Yes      | `false` (off), `moderate`, or `strict`                  |
| `offset`     | number | No       | Result offset, 0 or greater                             |

**Example**

```bash
curl -G "https://api.pullbay.com/duckduckgo/images" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d term="wallpaper cat" \
  -d region=us-en \
  -d safeSearch=moderate \
  -d offset=0
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "title": "Wallpaper Cat Photos, Download The BEST Free Wallpaper Cat Stock Photos",
      "image": "https://images.pexels.com/photos/20331914/pexels-photo-20331914.jpeg",
      "thumbnail": "https://tse1.mm.bing.net/th/id/OIP.oOmFm8pcnPy1pVPS46kxfQHaE8?r=0&pid=Api",
      "image_token": "a8b8c30ef6de5362d93dd6f5848ec661ae916727695b7e944138db4a60c5f6ca",
      "thumbnail_token": "036492c962e7830bd485903527a6caf439793418572197fbf6a743a8255f594b",
      "width": 6000,
      "height": 4000,
      "source": "Bing",
      "url": "https://www.pexels.com/search/Wallpaper%20cat/"
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of results returned, not the number of requests.

***

### Search News

Returns news articles for `term`. Use `time` to limit recency (`a`=any, `d`=day, `w`=week, `m`=month, `y`=year).

**Request**

```bash
GET /duckduckgo/news
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter    | Type   | Required | Description                                                               |
| ------------ | ------ | -------- | ------------------------------------------------------------------------- |
| `term`       | string | Yes      | Search term, 1–255 characters                                             |
| `region`     | string | Yes      | DuckDuckGo region code (e.g. `wt-wt`, `us-en`, `uk-en`)                   |
| `safeSearch` | string | Yes      | `false` (off), `moderate`, or `strict`                                    |
| `offset`     | number | No       | Result offset, 0 or greater                                               |
| `time`       | string | No       | Recency filter: `a` (any), `d` (day), `w` (week), `m` (month), `y` (year) |

**Example**

```bash
curl -G "https://api.pullbay.com/duckduckgo/news" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d term="Marvell Technology" \
  -d region=us-en \
  -d safeSearch=moderate \
  -d time=w
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "title": "Why Marvell Technology Stock Just Dropped",
      "excerpt": "Why Marvell's mad at Poet. Seems somebody let the cat out of the bag too soon...",
      "url": "https://www.fool.com/investing/2026/04/27/why-marvell-technology-stock-just-dropped/",
      "image": "https://g.foolcdn.com/image/?url=https%3A%2F%2Fg.foolcdn.com%2Feditorial%2Fimages%2F867388%2F5-red-stock-market-arrows-pointing-down.jpg",
      "date": 1777303200,
      "relativeTime": "21 hours ago",
      "syndicate": "bing",
      "isOld": false
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of articles returned, not the number of requests.

***

### Search Videos

Returns video search results for `term`.

**Request**

```bash
GET /duckduckgo/videos
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter    | Type   | Required | Description                                             |
| ------------ | ------ | -------- | ------------------------------------------------------- |
| `term`       | string | Yes      | Search term, 1–255 characters                           |
| `region`     | string | Yes      | DuckDuckGo region code (e.g. `wt-wt`, `us-en`, `uk-en`) |
| `safeSearch` | string | Yes      | `false` (off), `moderate`, or `strict`                  |
| `offset`     | number | No       | Result offset, 0 or greater                             |

**Example**

```bash
curl -G "https://api.pullbay.com/duckduckgo/videos" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d term="cooking skills" \
  -d region=us-en \
  -d safeSearch=moderate \
  -d offset=0
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "title": "Gordon Ramsay's Top Basic Cooking Skills | Ultimate Cookery Course FULL EPISODE",
      "description": "Gordon Ramsay demonstrates some basic cooking skills as well as some easy to do recipes.",
      "url": "https://www.youtube.com/watch?v=FTociictyyE",
      "image": "https://tse4.mm.bing.net/th/id/OVP.dTE0hC-DCsYuM6UsczwKpgHgFo?pid=Api",
      "duration": "23:03",
      "publishedOn": "YouTube",
      "published": "2019-11-07T17:00:05.0000000",
      "publisher": "Gordon Ramsay",
      "viewCount": 9252292
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of videos returned, not the number of requests.

***

## Object Schemas

### Search Result object

| Field         | Type   | Description                        |
| ------------- | ------ | ---------------------------------- |
| `title`       | string | Page title                         |
| `description` | string | Snippet/summary text from the page |
| `hostname`    | string | Domain the result was served from  |
| `icon`        | string | Favicon URL                        |
| `url`         | string | Link to the page                   |

### Image object

| Field              | Type    | Description                                    |
| ------------------ | ------- | ---------------------------------------------- |
| `title`            | string  | Image title                                    |
| `image`            | string  | Full-size image URL                            |
| `thumbnail`        | string  | Thumbnail image URL                            |
| `image_token`      | string  | Opaque token identifying the full-size image   |
| `thumbnail_token`  | string  | Opaque token identifying the thumbnail         |
| `width` / `height` | integer | Image dimensions in pixels                     |
| `source`           | string  | Originating image search backend (e.g. `Bing`) |
| `url`              | string  | Link to the page hosting the image             |

### News Article object

| Field          | Type    | Description                                                |
| -------------- | ------- | ---------------------------------------------------------- |
| `title`        | string  | Article headline                                           |
| `excerpt`      | string  | Short snippet of the article body                          |
| `url`          | string  | Link to the full article                                   |
| `image`        | string  | Associated image URL                                       |
| `date`         | integer | Publish timestamp, Unix seconds                            |
| `relativeTime` | string  | Human-readable relative publish time (e.g. `21 hours ago`) |
| `syndicate`    | string  | Syndication source (e.g. `bing`)                           |
| `isOld`        | boolean | Whether the article is flagged as stale/older content      |

### Video object

| Field         | Type               | Description                                       |
| ------------- | ------------------ | ------------------------------------------------- |
| `title`       | string             | Video title                                       |
| `description` | string             | Video description                                 |
| `url`         | string             | Link to the video                                 |
| `image`       | string             | Thumbnail/preview image URL                       |
| `duration`    | string             | Video duration, formatted `mm:ss` (or `hh:mm:ss`) |
| `publishedOn` | string             | Hosting platform (e.g. `YouTube`)                 |
| `published`   | string (date-time) | Publish timestamp                                 |
| `publisher`   | string             | Channel or publisher name                         |
| `viewCount`   | integer            | View count                                        |

***

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                  |
| ------ | ---------------------------------------- |
| `400`  | Bad request — missing/invalid parameters |
| `402`  | Insufficient credits                     |
| `404`  | No results found                         |

## Region Codes

* `region` accepts a DuckDuckGo-specific region code, not a plain ISO country code — most are `<country>-<language>` pairs (e.g. `us-en`, `uk-en`, `de-de`, `jp-jp`), with a few exceptions (`wt-wt` for worldwide/no region, `ct-ca`, `xa-ar`).
* Use `wt-wt` when you don't want to bias results to a specific region.

## Safe Search Levels

* `safeSearch` takes one of three values: `false` (safe search off), `moderate` (default-style filtering), or `strict` (maximum filtering). Note that the "off" value is the literal `false`, not a string.

## Pagination and Recency

* All four endpoints page via `offset` — there is no `page`, `cursor`, or `maxItems` parameter on this service. Increase `offset` by the number of results already consumed to fetch the next batch.
* `time` (on `search` and `news` only) restricts results to a recency window (`a`/`d`/`w`/`m`/`y`) rather than paging through results — it is not available on `images` or `videos`.


# Google News

Google News lets you list articles by category or run a free-text search. All endpoints are read-only `GET` requests and return public Google News data.

**Base URL:** `https://api.pullbay.com/`

## Endpoints

| Endpoint                    | Description                                  | Pagination          |
| --------------------------- | -------------------------------------------- | ------------------- |
| `GET /google-news/category` | List top articles for a Google News category | —                   |
| `GET /google-news/search`   | Search Google News articles by query         | maxItems (one-shot) |

***

## Get News By Category

Returns top news articles for a given Google News category. `languageCode` selects the locale edition (e.g. `US:en`, `GB:en`).

**Request**

```http
GET /google-news/category
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter      | Type   | Required | Description                                                                                                      |
| -------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------- |
| `category`     | string | Yes      | News category. One of `business`, `entertainment`, `health`, `latest`, `science`, `sport`, `technology`, `world` |
| `languageCode` | string | No       | Locale edition code, e.g. `US:en`, `GB:en`, `DE:de`, `JP:ja`                                                     |

**Example**

```bash
curl -G "https://api.pullbay.com/google-news/category" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d category=technology \
  -d languageCode=US:en
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "title": "The next phase of the Microsoft-OpenAI partnership",
      "articleUrl": "https://news.google.com/read/CBMimwFBVV95cUxNakYwXzBZNG85QlRVd01tNzEwaEM3djQxWnMya0FN",
      "publishedAt": "2026-04-27T13:07:55Z",
      "imageUrl": "/api/attachments/CC8iK0NnNXdhR2hQYWxjeGIxSmpNMEpOVFJDZkF4ampCU2dLTWdZQm9KVEVQQWM=-w64-h64-p-df-rw",
      "source": "The Official Microsoft Blog",
      "sourceIconUrl": "https://encrypted-tbn0.gstatic.com/faviconV2?url=https://blogs.microsoft.com",
      "author": "By Brendan Morrow"
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of articles returned, not the number of requests.

***

## Search News

Returns articles matching `query`. Set `resolveUrl=yes` to follow Google's redirect and return the publisher's direct URL instead of the `news.google.com` link.

**Request**

```http
GET /google-news/search
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter      | Type    | Required | Description                                                                  |
| -------------- | ------- | -------- | ---------------------------------------------------------------------------- |
| `query`        | string  | Yes      | Search query, 1–500 characters                                               |
| `languageCode` | string  | No       | Locale edition code, e.g. `US:en`, `GB:en`, `DE:de`, `JP:ja`                 |
| `resolveUrl`   | boolean | No       | `true` to resolve the publisher URL instead of the Google News redirect link |
| `maxItems`     | integer | No       | One-shot bulk pull, 1–100 results                                            |

**Example**

```bash
curl -G "https://api.pullbay.com/google-news/search" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d query="OpenAI partnership" \
  -d languageCode=US:en \
  -d resolveUrl=true \
  -d maxItems=20
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "title": "The next phase of the Microsoft-OpenAI partnership",
      "articleUrl": "https://news.google.com/read/CBMimwFBVV95cUxNakYwXzBZNG85QlRVd01tNzEwaEM3djQxWnMya0FN",
      "publishedAt": "2026-04-27T13:07:55Z",
      "imageUrl": "/api/attachments/CC8iK0NnNXdhR2hQYWxjeGIxSmpNMEpOVFJDZkF4ampCU2dLTWdZQm9KVEVQQWM=-w64-h64-p-df-rw",
      "source": "The Official Microsoft Blog",
      "sourceIconUrl": "https://encrypted-tbn0.gstatic.com/faviconV2?url=https://blogs.microsoft.com",
      "author": "By Brendan Morrow"
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Using `maxItems` to pull more results in one call costs proportionally more than a smaller pull.

***

## Object schemas

### GoogleNewsArticle object

| Field           | Type               | Description                                                                                     |
| --------------- | ------------------ | ----------------------------------------------------------------------------------------------- |
| `title`         | string             | Article headline                                                                                |
| `articleUrl`    | string             | Link to the article on `news.google.com` (or the resolved publisher URL when `resolveUrl=true`) |
| `publishedAt`   | string (date-time) | Publication timestamp                                                                           |
| `imageUrl`      | string, nullable   | Thumbnail image URL, often a relative `/api/attachments/...` path                               |
| `source`        | string             | Publisher/outlet name                                                                           |
| `sourceIconUrl` | string             | Favicon URL for the publisher                                                                   |
| `author`        | string, nullable   | Byline, when available                                                                          |

***

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                  |
| ------ | ---------------------------------------- |
| `400`  | Bad request — missing/invalid parameters |
| `402`  | Insufficient credits                     |
| `404`  | No articles found                        |

## Category and Language Codes

* `category` accepts one of `business`, `entertainment`, `health`, `latest`, `science`, `sport`, `technology`, `world`.
* `languageCode` combines a 2-letter country code and a language/locale tag, separated by a colon (e.g. `US:en`, `GB:en`, `BR:pt-419`, `JP:ja`). Pass the pair exactly as listed — country and language aren't independent fields here.
* If `languageCode` is omitted, results default to the `US:en` edition.

## Resolving Publisher URLs

* By default, `articleUrl` points to a `news.google.com/read/...` redirect link, not the original publisher page.
* Set `resolveUrl=true` on `/google-news/search` to have Pullbay follow that redirect server-side and return the final publisher URL instead.
* `/google-news/category` does not expose `resolveUrl` — its `articleUrl` is always the Google News redirect link.


# Google Maps

Google Maps: place search, place details, and place reviews. All endpoints are read-only `GET` requests and return public Google Maps data.

**Base URL:** `https://api.pullbay.com/`

## Endpoints

| Endpoint                         | Description                                  | Pagination        |
| -------------------------------- | -------------------------------------------- | ----------------- |
| `GET /google-maps/place/search`  | Search Google Maps places by free-text query | Offset / maxItems |
| `GET /google-maps/place/{fid}`   | Get full details for a place by FID          | —                 |
| `GET /google-maps/place/reviews` | Get reviews for a place by Google Maps URL   | Cursor / maxItems |

***

## Search Places

Search Google Maps for places by free-text `query`. Optional geo filters (`latitude`, `longitude`, `zoom`) bias results toward a specific map viewport.

**Request**

```bash
GET /google-maps/place/search
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter      | Type    | Required | Description                                                                   |
| -------------- | ------- | -------- | ----------------------------------------------------------------------------- |
| `query`        | string  | Yes      | Free-text search term, 1–200 characters (e.g. `pizza near soho new york`)     |
| `countryCode`  | string  | No       | 2–5 character country code to bias results                                    |
| `languageCode` | string  | No       | 2–10 character language code for localized text                               |
| `latitude`     | string  | No       | Latitude to center the search viewport (numeric string)                       |
| `longitude`    | string  | No       | Longitude to center the search viewport (numeric string)                      |
| `zoom`         | string  | No       | Map zoom level to use alongside `latitude`/`longitude` (numeric string)       |
| `offset`       | string  | No       | Numeric offset string to paginate results. Mutually exclusive with `maxItems` |
| `maxItems`     | integer | No       | One-shot bulk pull, 1–500 results. Mutually exclusive with `offset`           |

**Example**

```bash
curl -G "https://api.pullbay.com/google-maps/place/search" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d query="pizza near soho new york" \
  -d countryCode=us \
  -d offset=0
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "url": "https://www.google.com/maps/place/data=!3m1!4b1!4m2!3m1!1s0x89c25985f7ec1beb:0x3c39f655da433d9d",
      "title": "Prince Street Pizza",
      "description": null,
      "address": "27 Prince St, New York, NY 10012",
      "neighborhood": "Manhattan",
      "street": "27 Prince St",
      "district": "New York",
      "postalCode": "10012",
      "city": "New York",
      "country": "United States",
      "location": { "lat": 40.7230777, "lng": -73.9945444 },
      "website": "https://locations.princestreetpizza.com/new-york",
      "phone": "(212) 966-4100",
      "price": "$10–20",
      "totalScore": 4.4,
      "placeId": "ChIJ6xvs94VZwokRnT1D2lX2OTw",
      "fid": "0x89c25985f7ec1beb:0x3c39f655da433d9d",
      "reviewsCount": 9039,
      "categories": ["Pizza restaurant"],
      "openingHours": [],
      "additionalInfo": {}
    }
  ],
  "pagination": { "page": 1, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Using `maxItems` to pull more results in one call costs proportionally more than a single page.

***

## Get Place

Returns full details for a Google Maps place identified by its FID.

**Request**

```bash
GET /google-maps/place/{fid}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter      | Type   | Required | Description                                                                                                       |
| -------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `fid`          | string | Yes      | Google Maps feature ID, of the form `0x...:0x...` (e.g. `0x47e671d877937b0f:0xb975fcfa192f84d4`) — path parameter |
| `countryCode`  | string | No       | 2–5 character country code                                                                                        |
| `languageCode` | string | No       | 2–10 character language code for localized text                                                                   |

**Example**

```bash
curl -G "https://api.pullbay.com/google-maps/place/0x47e671d877937b0f:0xb975fcfa192f84d4" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d languageCode=en
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "url": "https://www.google.com/maps/preview/place/Louvre+Museum",
      "title": "Louvre Museum",
      "address": "Louvre Museum, 75001 Paris, France",
      "neighborhood": null,
      "street": null,
      "district": "Paris",
      "postalCode": "75001",
      "city": null,
      "location": { "lat": 40.7230777, "lng": -73.9945444 },
      "website": "https://www.louvre.fr/",
      "phone": "+33 1 40 20 53 17",
      "phoneUnformatted": "+33140205317",
      "plusCode": "V86Q+63 Paris, France",
      "price": null,
      "totalScore": 4.7,
      "placeId": "ChIJD3uTd9hx5kcR1IQvGfr8dbk",
      "cid": "-5082878453323168556",
      "fid": "0x47e671d877937b0f:0xb975fcfa192f84d4",
      "reviewsCount": 366153,
      "reviewsDistribution": {
        "oneStar": 5180,
        "twoStar": 3447,
        "threeStar": 13286,
        "fourStar": 52471,
        "fiveStar": 291769
      },
      "categories": ["Art museum", "Museum", "Tourist attraction"],
      "additionalInfo": {}
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

## Get Place Reviews

Returns reviews for a place identified by its full Google Maps URL.

**Request**

```bash
GET /google-maps/place/reviews
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Description                                                         |
| ---------- | ------- | -------- | ------------------------------------------------------------------- |
| `placeUrl` | string  | Yes      | Full Google Maps place URL, 10–1000 characters                      |
| `maxItems` | integer | No       | One-shot bulk pull, 1–500 results. Mutually exclusive with `cursor` |
| `cursor`   | string  | No       | Opaque pagination cursor. Mutually exclusive with `maxItems`        |

**Example**

```bash
curl -G "https://api.pullbay.com/google-maps/place/reviews" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d placeUrl="https://www.google.com/maps/place/data=!3m1!4b1!4m2!3m1!1s0x89c25985f7ec1beb:0x3c39f655da433d9d" \
  -d maxItems=20
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "Ci9DQUlRQUNvZENodHljRjlvT25OdmExZHZVSEJPYjJkRGIxVXpiRFIzYXpWUmFFRRAB",
      "link": "https://www.google.com/maps/reviews/data=!4m8!14m7!1m6!2m5!1sCi9DQUlRQUNvZENodHljRjlvT25OdmExZHZVSEJPYjJkRGIxVXpiRFIzYXpWUmFFRRAB",
      "text": "Loved the experience",
      "stars": 5,
      "date": 1777380220000,
      "dateIso": "2026-04-28T12:43:40.000Z",
      "likes": null,
      "photos": [],
      "reviewer": {
        "name": "Jane Doe",
        "photo": "https://example.com/reviewer-photo.jpg",
        "url": "https://www.google.com/maps/contrib/000000000000000000000?hl=en-US",
        "totalReviews": 2,
        "totalPhotos": 7
      },
      "ownersResponse": {},
      "details": {
        "Visited on": "Weekend",
        "Wait time": "10–30 min",
        "Reservation recommended": true
      }
    }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of reviews returned, not the number of requests.

***

## Object schemas

### Place search result object

| Field            | Type             | Description                                                                              |
| ---------------- | ---------------- | ---------------------------------------------------------------------------------------- |
| `url`            | string           | Google Maps URL for the place                                                            |
| `title`          | string           | Place name                                                                               |
| `description`    | string, nullable | Short description, if available                                                          |
| `address`        | string           | Full formatted address                                                                   |
| `neighborhood`   | string, nullable | Neighborhood name                                                                        |
| `street`         | string, nullable | Street address                                                                           |
| `district`       | string, nullable | District or city-level area                                                              |
| `postalCode`     | string, nullable | Postal code                                                                              |
| `city`           | string, nullable | City name                                                                                |
| `country`        | string, nullable | Country name                                                                             |
| `location`       | object           | Latitude/longitude pair — see Location object                                            |
| `website`        | string, nullable | Place's website URL                                                                      |
| `phone`          | string, nullable | Formatted phone number                                                                   |
| `price`          | string, nullable | Price range indicator (e.g. `$10–20`)                                                    |
| `totalScore`     | number           | Average rating                                                                           |
| `placeId`        | string           | Google Place ID                                                                          |
| `fid`            | string           | Google Maps feature ID, of the form `0x...:0x...` — use this to fetch full place details |
| `reviewsCount`   | integer          | Total number of reviews                                                                  |
| `categories`     | array            | Place category labels                                                                    |
| `openingHours`   | array            | Opening hours per weekday                                                                |
| `additionalInfo` | object           | Grouped feature flags (e.g. Service options, Accessibility, Offerings)                   |

### Location object

| Field | Type   | Description |
| ----- | ------ | ----------- |
| `lat` | number | Latitude    |
| `lng` | number | Longitude   |

### Place object

| Field                 | Type             | Description                                                               |
| --------------------- | ---------------- | ------------------------------------------------------------------------- |
| `url`                 | string           | Google Maps URL for the place                                             |
| `title`               | string           | Place name                                                                |
| `address`             | string           | Full formatted address                                                    |
| `neighborhood`        | string, nullable | Neighborhood name                                                         |
| `street`              | string, nullable | Street address                                                            |
| `district`            | string, nullable | District or city-level area                                               |
| `postalCode`          | string, nullable | Postal code                                                               |
| `city`                | string, nullable | City name                                                                 |
| `location`            | object           | Latitude/longitude pair — see Location object                             |
| `website`             | string, nullable | Place's website URL                                                       |
| `phone`               | string, nullable | Formatted phone number                                                    |
| `phoneUnformatted`    | string, nullable | Phone number without formatting                                           |
| `plusCode`            | string, nullable | Google Plus Code                                                          |
| `price`               | string, nullable | Price range indicator                                                     |
| `totalScore`          | number           | Average rating                                                            |
| `placeId`             | string           | Google Place ID                                                           |
| `cid`                 | string           | Google Maps client ID                                                     |
| `fid`                 | string           | Google Maps feature ID, of the form `0x...:0x...`                         |
| `reviewsCount`        | integer          | Total number of reviews                                                   |
| `reviewsDistribution` | object           | Review count broken down by star rating — see Reviews distribution object |
| `categories`          | array            | Place category labels                                                     |
| `additionalInfo`      | object           | Grouped feature flags (e.g. Service options, Accessibility, Amenities)    |

### Reviews distribution object

| Field       | Type    | Description             |
| ----------- | ------- | ----------------------- |
| `oneStar`   | integer | Count of 1-star reviews |
| `twoStar`   | integer | Count of 2-star reviews |
| `threeStar` | integer | Count of 3-star reviews |
| `fourStar`  | integer | Count of 4-star reviews |
| `fiveStar`  | integer | Count of 5-star reviews |

### Place review object

| Field            | Type               | Description                                                   |
| ---------------- | ------------------ | ------------------------------------------------------------- |
| `id`             | string             | Review ID                                                     |
| `link`           | string             | Permalink to the review                                       |
| `text`           | string             | Review body                                                   |
| `stars`          | integer            | Rating, 1–5                                                   |
| `date`           | integer            | Posted timestamp, Unix milliseconds                           |
| `dateIso`        | string (date-time) | Posted timestamp, ISO 8601                                    |
| `likes`          | integer, nullable  | Number of likes on the review                                 |
| `photos`         | array              | Photo URLs attached to the review                             |
| `reviewer`       | object             | Reviewer who left the review — see Reviewer object            |
| `ownersResponse` | object             | Owner's response to the review (empty object when none)       |
| `details`        | object             | Free-form key/value metadata (e.g. `Visited on`, `Wait time`) |

### Reviewer object

| Field          | Type    | Description                                |
| -------------- | ------- | ------------------------------------------ |
| `name`         | string  | Reviewer's display name                    |
| `photo`        | string  | Reviewer's profile photo URL               |
| `url`          | string  | Link to the reviewer's Google Maps profile |
| `totalReviews` | integer | Total reviews the reviewer has written     |
| `totalPhotos`  | integer | Total photos the reviewer has uploaded     |

***

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                  |
| ------ | ---------------------------------------- |
| `400`  | Bad request — missing/invalid parameters |
| `402`  | Insufficient credits                     |
| `404`  | Place or review set not found            |

## Finding FIDs and Place URLs

* **FID**: the feature ID shown in `GoogleMapsPlaceSearchResult.fid` and `GoogleMapsPlace.fid`, of the form `0x47e671d877937b0f:0xb975fcfa192f84d4`. Run a search first to discover a place's FID, then pass it to `/google-maps/place/{fid}` for full details.
* **Place URL**: the full `google.com/maps/place/...` URL for a place, also returned as `url` on search and detail responses. Pass it as-is to `/google-maps/place/reviews`.

## Pagination Notes

* `/google-maps/place/search` paginates by numeric `offset` (a string), or accepts `maxItems` for a one-shot bulk pull — the two are mutually exclusive.
* `/google-maps/place/reviews` paginates by opaque `cursor`, or accepts `maxItems` for a one-shot bulk pull — the two are mutually exclusive.
* `/google-maps/place/{fid}` returns a single place and has no pagination.

## Country and Language Codes

* `countryCode` accepts a short country code (2–5 characters) used to bias search results regionally.
* `languageCode` accepts a 2–10 character language code for localized place names and text (e.g. `en`, `en-us`, `fr`).
* If omitted, both default to Google's standard locale resolution for the request.


# Google Play

Google Play Store: app lookup, search, similar apps, reviews, permissions, and data safety. All endpoints are read-only `GET` requests and return public Google Play data — no app of your own is required.

**Base URL:** `https://api.pullbay.com/`

## Endpoints

| Endpoint                       | Description                            | Pagination        |
| ------------------------------ | -------------------------------------- | ----------------- |
| `GET /google-play/apps`        | Get a single app by App ID             | —                 |
| `GET /google-play/search`      | Search apps by term                    | —                 |
| `GET /google-play/similar`     | Get apps similar to a given app        | —                 |
| `GET /google-play/reviews`     | Get reviews for an app                 | Cursor / maxItems |
| `GET /google-play/permissions` | Get permissions requested by an app    | —                 |
| `GET /google-play/data-safety` | Get the Data Safety section for an app | —                 |

***

## Get App

Returns full details for a single app on the Google Play Store, identified by its package name.

**Request**

```http
GET /google-play/apps
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                                                                  |
| --------- | ------ | -------- | -------------------------------------------------------------------------------------------- |
| `appId`   | string | Yes      | Package name, max 255 characters, letters/digits/`.`/`_` only (e.g. `com.instagram.android`) |
| `country` | string | No       | 2-letter ISO country code for the storefront (e.g. `us`)                                     |
| `lang`    | string | No       | Language code for localized text (e.g. `en`, `en-us`)                                        |

**Example**

```bash
curl -G "https://api.pullbay.com/google-play/apps" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d appId=com.instagram.android \
  -d country=us
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "title": "Instagram",
      "appId": "com.instagram.android",
      "url": "/api/apps/com.instagram.android",
      "icon": "https://play-lh.googleusercontent.com/VRMWkE5p3CkWhJs6nv-9ZsLAs1QOg5ob1_3qg-rckwYW7yp1fMrYZqnEFpk0IoVP4LM",
      "headerImage": "https://play-lh.googleusercontent.com/CgUPurtUp8SB709wsQcKLMv6JdIh1em0ka_58V69AorznLv42TV39etS2rt28wv9Rz8",
      "summary": "Create &amp; share photos, stories, &amp; reels with friends you love",
      "description": "Little moments lead to big friendships. Share yours on Instagram.",
      "descriptionHTML": "Little moments lead to big friendships. Share yours on Instagram. <br>—From Meta",
      "score": 4.0182447,
      "scoreText": "4.0",
      "ratings": 167461256,
      "reviews": 4568306,
      "histogram": { "1": 30279668, "2": 5233417, "3": 7039797, "4": 13507336, "5": 111401011 },
      "free": true,
      "price": 0,
      "priceText": "Free",
      "currency": "USD",
      "offersIAP": true,
      "adSupported": true,
      "installs": "5,000,000,000+",
      "minInstalls": 5000000000,
      "maxInstalls": 7427343782,
      "androidVersion": "VARY",
      "androidVersionText": "Varies with device",
      "updated": 1777328205000,
      "genre": "Social",
      "genreId": "SOCIAL",
      "developer": "Instagram",
      "developerId": "Instagram",
      "developerEmail": "instagram-android@meta.com",
      "developerWebsite": "http://help.instagram.com/",
      "developerInternalID": "Instagram",
      "privacyPolicy": "http://instagram.com/legal/privacy/",
      "playstoreUrl": "https://play.google.com/store/apps/details?id=com.instagram.android&hl=en&gl=us",
      "screenshots": ["https://play-lh.googleusercontent.com/hZ2Ob6d0MqLse2lIdNOGgl93H5y82uhX4hC6mWDUpdwK8LjXAunweDQZ6Hocy0riv4KX0lTIzKJz6lhRDV_BiHg"]
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

## Search Apps

Search the Google Play Store for apps matching a term. Returns lightweight summaries.

**Request**

```http
GET /google-play/search
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                  |
| --------- | ------ | -------- | -------------------------------------------- |
| `search`  | string | Yes      | Search term, 1–255 characters                |
| `price`   | string | No       | Filter by pricing: `all`, `free`, or `paid`  |
| `country` | string | No       | 2-letter ISO country code for the storefront |
| `lang`    | string | No       | Language code for localized text             |

**Example**

```bash
curl -G "https://api.pullbay.com/google-play/search" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d search="photo editor" \
  -d price=free \
  -d country=us
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "title": "Instagram",
      "appId": "com.instagram.android",
      "url": "/api/apps/com.instagram.android",
      "icon": "https://play-lh.googleusercontent.com/VRMWkE5p3CkWhJs6nv-9ZsLAs1QOg5ob1_3qg-rckwYW7yp1fMrYZqnEFpk0IoVP4LM",
      "summary": "Create &amp; share photos, stories, &amp; reels with friends you love",
      "score": 4.0182447,
      "scoreText": "4.0",
      "free": true,
      "price": 0,
      "currency": "USD",
      "playstoreUrl": "https://play.google.com/store/apps/details?id=com.instagram.android",
      "developer": "Instagram",
      "developerId": "Instagram"
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of results returned, not the number of requests.

***

## Get Similar Apps

Returns apps Google Play surfaces as similar to the supplied app.

**Request**

```http
GET /google-play/similar
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                                   |
| --------- | ------ | -------- | ------------------------------------------------------------- |
| `appId`   | string | Yes      | Package name, max 255 characters, letters/digits/`.`/`_` only |

**Example**

```bash
curl -G "https://api.pullbay.com/google-play/similar" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d appId=com.instagram.android
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "title": "Instagram",
      "appId": "com.instagram.android",
      "url": "/api/apps/com.instagram.android",
      "icon": "https://play-lh.googleusercontent.com/VRMWkE5p3CkWhJs6nv-9ZsLAs1QOg5ob1_3qg-rckwYW7yp1fMrYZqnEFpk0IoVP4LM",
      "summary": "Create &amp; share photos, stories, &amp; reels with friends you love",
      "score": 4.0182447,
      "scoreText": "4.0",
      "free": true,
      "price": 0,
      "currency": "USD",
      "playstoreUrl": "https://play.google.com/store/apps/details?id=com.instagram.android",
      "developer": "Instagram",
      "developerId": "Instagram"
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

## Get Reviews

Returns reviews for an app. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).

**Request**

```http
GET /google-play/reviews
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Description                                                                           |
| ---------- | ------- | -------- | ------------------------------------------------------------------------------------- |
| `appId`    | string  | Yes      | Package name, max 255 characters, letters/digits/`.`/`_` only                         |
| `country`  | string  | No       | 2-letter ISO country code for the storefront                                          |
| `lang`     | string  | No       | Language code for localized text                                                      |
| `sort`     | string  | No       | `NEWEST`, `RATING`, or `HELPFULNESS`                                                  |
| `cursor`   | string  | No       | Opaque pagination cursor from a previous response. Mutually exclusive with `maxItems` |
| `maxItems` | integer | No       | One-shot bulk pull, 1–500 results. Mutually exclusive with `cursor`                   |

**Example**

```bash
curl -G "https://api.pullbay.com/google-play/reviews" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d appId=com.instagram.android \
  -d country=us \
  -d sort=NEWEST
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "045b1b4e-b207-40f7-8a9e-d4e81664965e",
      "userName": "Jane Doe",
      "userImage": "https://example.com/reviewer-photo.jpg",
      "date": "2025-01-12T22:37:55.959Z",
      "score": 1,
      "scoreText": "1",
      "url": "https://play.google.com/store/apps/details?id=com.instagram.android&reviewId=045b1b4e-b207-40f7-8a9e-d4e81664965e",
      "title": null,
      "text": "The new update is so buggy.",
      "replyDate": null,
      "replyText": null,
      "version": "362.0.0.33.241",
      "thumbsUp": 10224,
      "country": "US",
      "criterias": [
        { "criteria": "vaf_mark_message_as_read", "rating": 1 }
      ]
    }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of reviews returned, not the number of requests.

***

## Get Permissions

Returns the list of Android permissions the app requests, grouped by category.

**Request**

```http
GET /google-play/permissions
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                                   |
| --------- | ------ | -------- | ------------------------------------------------------------- |
| `appId`   | string | Yes      | Package name, max 255 characters, letters/digits/`.`/`_` only |

**Example**

```bash
curl -G "https://api.pullbay.com/google-play/permissions" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d appId=com.instagram.android
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    { "permission": "find accounts on the device", "type": "Identity" }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

## Get Data Safety

Returns the developer-declared Data Safety information: data shared, data collected, security practices, and privacy policy URL.

**Request**

```http
GET /google-play/data-safety
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                                   |
| --------- | ------ | -------- | ------------------------------------------------------------- |
| `appId`   | string | Yes      | Package name, max 255 characters, letters/digits/`.`/`_` only |

**Example**

```bash
curl -G "https://api.pullbay.com/google-play/data-safety" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d appId=com.instagram.android
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "sharedData": [
        { "data": "Name", "purpose": "Fraud prevention, security, and compliance", "type": "Personal info", "optional": false }
      ],
      "collectedData": [
        { "data": "Name", "purpose": "Fraud prevention, security, and compliance", "type": "Personal info", "optional": false }
      ],
      "securityPractices": [
        { "practice": "Data is encrypted in transit", "description": "Your data is transferred over a secure connection" }
      ],
      "privacyPolicyUrl": "http://instagram.com/legal/privacy/"
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

## Object schemas

### Application object

| Field                                                                                       | Type                               | Description                                     |
| ------------------------------------------------------------------------------------------- | ---------------------------------- | ----------------------------------------------- |
| `title`                                                                                     | string                             | App name                                        |
| `appId`                                                                                     | string                             | Package name                                    |
| `url`                                                                                       | string                             | Relative API URL for the app detail endpoint    |
| `icon`                                                                                      | string                             | Icon image URL                                  |
| `headerImage`                                                                               | string                             | Feature graphic image URL                       |
| `summary`                                                                                   | string                             | Short tagline shown on the store listing        |
| `description` / `descriptionHTML`                                                           | string                             | Full description, plain text and HTML           |
| `score` / `scoreText`                                                                       | number / string                    | Average rating, numeric and display string      |
| `ratings` / `reviews`                                                                       | integer                            | Total rating count and total review count       |
| `histogram`                                                                                 | object                             | Rating counts keyed by star value (`1`–`5`)     |
| `free` / `price` / `priceText` / `currency`                                                 | boolean / number / string / string | Pricing info                                    |
| `offersIAP`                                                                                 | boolean                            | Whether the app offers in-app purchases         |
| `adSupported`                                                                               | boolean                            | Whether the app shows ads                       |
| `installs` / `minInstalls` / `maxInstalls`                                                  | string / integer / integer         | Install count range, display and numeric bounds |
| `androidVersion` / `androidVersionText`                                                     | string                             | Minimum Android version, raw and display form   |
| `updated`                                                                                   | integer                            | Last update timestamp (Unix ms)                 |
| `genre` / `genreId`                                                                         | string                             | Category name and ID                            |
| `developer` / `developerId` / `developerEmail` / `developerWebsite` / `developerInternalID` | string                             | Developer identity and contact info             |
| `privacyPolicy`                                                                             | string                             | Developer's privacy policy URL                  |
| `playstoreUrl`                                                                              | string                             | Public Google Play listing URL                  |
| `screenshots`                                                                               | array                              | Screenshot image URLs                           |

### Application summary object

Lightweight app reference returned by search and similar-apps endpoints.

| Field                         | Type                      | Description                                  |
| ----------------------------- | ------------------------- | -------------------------------------------- |
| `title`                       | string                    | App name                                     |
| `appId`                       | string                    | Package name                                 |
| `url`                         | string                    | Relative API URL for the app detail endpoint |
| `icon`                        | string                    | Icon image URL                               |
| `summary`                     | string                    | Short tagline shown on the store listing     |
| `score` / `scoreText`         | number / string           | Average rating, numeric and display string   |
| `free` / `price` / `currency` | boolean / number / string | Pricing info                                 |
| `playstoreUrl`                | string                    | Public Google Play listing URL               |
| `developer` / `developerId`   | string                    | Developer name and ID                        |

### Review object

| Field                     | Type                      | Description                                                      |
| ------------------------- | ------------------------- | ---------------------------------------------------------------- |
| `id`                      | string                    | Review ID                                                        |
| `userName`                | string                    | Reviewer's display name                                          |
| `userImage`               | string                    | Reviewer's avatar URL                                            |
| `date`                    | string (date-time)        | When the review was posted                                       |
| `score` / `scoreText`     | integer / string          | Rating, 1–5, numeric and display string                          |
| `url`                     | string                    | Permalink to the review                                          |
| `title`                   | string, nullable          | Review title (often absent on Google Play)                       |
| `text`                    | string                    | Review body                                                      |
| `replyDate` / `replyText` | string / string, nullable | Developer's reply timestamp and text, if any                     |
| `version`                 | string                    | App version the review was left for                              |
| `thumbsUp`                | integer                   | Helpful-vote count                                               |
| `country`                 | string                    | Storefront country the review was posted on                      |
| `criterias`               | array of Review criteria  | Rating breakdown by sub-criteria, if the developer collects them |

### Review criteria object

| Field      | Type    | Description                                                            |
| ---------- | ------- | ---------------------------------------------------------------------- |
| `criteria` | string  | Internal name of the rated criterion (e.g. `vaf_mark_message_as_read`) |
| `rating`   | integer | Rating given for that specific criterion                               |

### Permission object

| Field        | Type   | Description                                                                |
| ------------ | ------ | -------------------------------------------------------------------------- |
| `permission` | string | Human-readable permission description (e.g. `find accounts on the device`) |
| `type`       | string | Permission category (e.g. `Identity`, `Location`)                          |

### Data Safety object

| Field               | Type                       | Description                            |
| ------------------- | -------------------------- | -------------------------------------- |
| `sharedData`        | array of Data Safety item  | Data types shared with third parties   |
| `collectedData`     | array of Data Safety item  | Data types collected by the app        |
| `securityPractices` | array of Security practice | Declared security practices            |
| `privacyPolicyUrl`  | string                     | Link to the developer's privacy policy |

### Data Safety item object

| Field      | Type    | Description                                       |
| ---------- | ------- | ------------------------------------------------- |
| `data`     | string  | Type of data (e.g. `Name`)                        |
| `purpose`  | string  | Stated purpose for collecting or sharing the data |
| `type`     | string  | Data category (e.g. `Personal info`)              |
| `optional` | boolean | Whether providing this data is optional           |

### Security practice object

| Field         | Type   | Description                                                         |
| ------------- | ------ | ------------------------------------------------------------------- |
| `practice`    | string | Name of the security practice (e.g. `Data is encrypted in transit`) |
| `description` | string | Explanation of the practice                                         |

***

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                  |
| ------ | ---------------------------------------- |
| `400`  | Bad request — missing/invalid parameters |
| `402`  | Insufficient credits                     |
| `404`  | App or review set not found              |

## Finding App IDs

* **App ID**: the Android package name, visible in a Google Play URL, e.g. `https://play.google.com/store/apps/details?id=com.instagram.android` → `com.instagram.android`.
* Package names are dot-separated identifiers made up of letters, digits, periods, and underscores — there is no separate numeric ID like on the App Store.

## Pagination Notes

* `/google-play/reviews` is the only paginated endpoint in this service. It uses **cursor-based** pagination: pass the `cursor` value from a previous response's `pagination.cursor` field to fetch the next page.
* Alternatively, use `maxItems` for a one-shot bulk pull of up to 500 reviews in a single call. `cursor` and `maxItems` are mutually exclusive — pick one strategy per request.
* All other endpoints in this service return a complete result set in one call and have no pagination parameters.

## Country and Language Codes

* `country` accepts any 2-letter ISO 3166-1 alpha-2 storefront code (`us`, `gb`, `jp`, `de`, ...).
* `lang` accepts a 2–3 letter ISO 639 language code, optionally with a region subtag (`en`, `en-us`, `pt-br`).
* If omitted, both default to the US storefront in English.


# Google Search

Google Search: web search results. A single read-only `GET` endpoint that returns organic Google search results for a query.

**Base URL:** `https://api.pullbay.com/`

## Endpoints

| Endpoint                    | Description               | Pagination      |
| --------------------------- | ------------------------- | --------------- |
| `GET /google-search/search` | Search the web via Google | Page / maxItems |

***

### Search

Returns Google search results for `query`.

**Request**

```bash
GET /google-search/search
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Description                                                       |
| ---------- | ------- | -------- | ----------------------------------------------------------------- |
| `query`    | string  | Yes      | Search query, 1–1000 characters                                   |
| `safe`     | string  | No       | SafeSearch filter level                                           |
| `hl`       | string  | No       | Interface/results language code (e.g. `en`)                       |
| `country`  | string  | No       | 2-letter ISO country code for the search region                   |
| `count`    | string  | No       | Results per page; only `10` is supported                          |
| `noEncode` | string  | No       | Skip URL-encoding the query before sending it upstream            |
| `page`     | integer | No       | Page number, 1+. Mutually exclusive with `maxItems`               |
| `maxItems` | integer | No       | One-shot bulk pull, 1–200 results. Mutually exclusive with `page` |

**Example**

```bash
curl -G "https://api.pullbay.com/google-search/search" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d query="openai" \
  -d country=us \
  -d hl=en \
  -d page=1
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "searchResult",
      "title": "OpenAI | OpenAI",
      "link": "https://openai.com/",
      "snippet": "We believe our research will eventually lead to artificial general intelligence, a system that can solve human-level problems."
    }
  ],
  "pagination": { "page": 1, "hasNextPage": true, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Using `maxItems` to pull more results in one call costs proportionally more than a single page.

***

## Object schemas

### GoogleSearchResult object

| Field     | Type   | Description                                    |
| --------- | ------ | ---------------------------------------------- |
| `type`    | string | Result type, always `searchResult`             |
| `title`   | string | Page title as shown in search results          |
| `link`    | string | URL of the result                              |
| `snippet` | string | Descriptive text excerpt shown under the title |

***

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                  |
| ------ | ---------------------------------------- |
| `400`  | Bad request — missing/invalid parameters |
| `402`  | Insufficient credits                     |
| `404`  | App, developer, or review set not found  |

## Pagination and Bulk Pulls

* Use `page` to walk through results one page at a time (`count=10` results per page); `pagination.hasNextPage` in the response tells you whether to keep going.
* Use `maxItems` instead of `page` to pull a larger result set in a single call (up to 200 results). Don't pass both — they're mutually exclusive.
* `count` currently only accepts the value `10` and can typically be omitted.

## Language and Region Codes

* `hl` accepts a 2–3 letter ISO 639 language code (`en`, `es`, `ja`, ...) to control the language of result snippets and UI strings.
* `country` accepts a 2-letter ISO 3166-1 alpha-2 code (`us`, `gb`, `jp`, ...) to bias results to a search region.
* If omitted, both default to a US/English search context.


# Instagram

Instagram: users, posts, reels, stories, and comments. All endpoints are read-only `GET` requests and return public Instagram data — no account of your own is required.

**Base URL:** `https://api.pullbay.com/`

## Endpoints

| Endpoint                                | Description                           | Pagination        |
| --------------------------------------- | ------------------------------------- | ----------------- |
| **Users**                               |                                       |                   |
| `GET /instagram/userid/{handle}`        | Resolve a handle to a numeric user ID | —                 |
| `GET /instagram/user/handle/{handle}`   | Get a user profile by handle          | —                 |
| `GET /instagram/user/{id}`              | Get a user profile by numeric ID      | —                 |
| **Posts & Reels**                       |                                       |                   |
| `GET /instagram/user/{id}/posts`        | Get posts published by a user         | Cursor / maxItems |
| `GET /instagram/user/{id}/posts/tagged` | Get posts a user is tagged in         | Cursor / maxItems |
| `GET /instagram/user/{id}/reels`        | Get reels published by a user         | Cursor / maxItems |
| `GET /instagram/post/code/{code}`       | Get a single post by shortcode        | —                 |
| **Stories**                             |                                       |                   |
| `GET /instagram/user/{id}/stories`      | Get a user's currently-active stories | —                 |
| **Comments**                            |                                       |                   |
| `GET /instagram/post/{id}/comments`     | Get comments on a post                | Cursor / maxItems |

***

### Resolve User ID

Returns whether a handle is available and, if it exists, the resolved Instagram user ID. This is the common entry point for the rest of the service — see [Resolving a handle to a user ID](#resolving-a-handle-to-a-user-id) below.

**Request**

```http
GET /instagram/userid/{handle}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                                        |
| --------- | ------ | -------- | ------------------------------------------------------------------ |
| `handle`  | string | Yes      | Instagram handle, without `@`, 1–100 characters (e.g. `instagram`) |

**Example**

```bash
curl -G "https://api.pullbay.com/instagram/userid/instagram" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "isAvailable": true,
      "handle": "instagram",
      "id": "25025320"
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get User By Handle

Returns an Instagram user profile by handle (without `@`).

**Request**

```http
GET /instagram/user/handle/{handle}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                                        |
| --------- | ------ | -------- | ------------------------------------------------------------------ |
| `handle`  | string | Yes      | Instagram handle, without `@`, 1–100 characters (e.g. `instagram`) |

**Example**

```bash
curl -G "https://api.pullbay.com/instagram/user/handle/instagram" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "user",
      "isAvailable": true,
      "id": "25025320",
      "username": "instagram",
      "fullName": "Instagram",
      "biography": "Discover what's new on Instagram 🔎✨",
      "profilePicUrl": "https://instagram.com/static/images/profile.jpg",
      "website": "https://about.instagram.com",
      "category": "App page",
      "cityName": null,
      "isPrivate": false,
      "isVerified": true,
      "isBusiness": true,
      "hasVideos": true,
      "hasGuides": false,
      "postCount": 8200,
      "followerCount": 689000000,
      "followingCount": 264,
      "totalReelsCount": 1850,
      "bioLinks": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get User By ID

Returns an Instagram user profile by numeric user ID.

**Request**

```http
GET /instagram/user/{id}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                                   |
| --------- | ------ | -------- | ------------------------------------------------------------- |
| `id`      | string | Yes      | Numeric Instagram user ID, 1–100 characters (e.g. `25025320`) |

**Example**

```bash
curl -G "https://api.pullbay.com/instagram/user/25025320" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "user",
      "isAvailable": true,
      "id": "25025320",
      "username": "instagram",
      "fullName": "Instagram",
      "biography": "Discover what's new on Instagram 🔎✨",
      "profilePicUrl": "https://instagram.com/static/images/profile.jpg",
      "website": "https://about.instagram.com",
      "category": "App page",
      "cityName": null,
      "isPrivate": false,
      "isVerified": true,
      "isBusiness": true,
      "hasVideos": true,
      "hasGuides": false,
      "postCount": 8200,
      "followerCount": 689000000,
      "followingCount": 264,
      "totalReelsCount": 1850,
      "bioLinks": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get User Posts

Returns posts published by an Instagram user. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).

**Request**

```http
GET /instagram/user/{id}/posts
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Description                                                                    |
| ---------- | ------- | -------- | ------------------------------------------------------------------------------ |
| `id`       | string  | Yes      | Numeric Instagram user ID, 1–100 characters                                    |
| `maxItems` | integer | No       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`            |
| `cursor`   | string  | No       | Pagination cursor from a previous response. Mutually exclusive with `maxItems` |

**Example**

```bash
curl -G "https://api.pullbay.com/instagram/user/25025320/posts" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d maxItems=50
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "photo",
      "id": "3881985252256633854",
      "code": "CzABCdef123",
      "url": "https://www.instagram.com/p/CzABCdef123/",
      "caption": "Check out our latest update!",
      "createdAt": 1714560000,
      "likeCount": 12500,
      "commentCount": 320,
      "isAvailable": true,
      "isCarousel": false,
      "isVideo": false,
      "isPinned": false,
      "isPaidPartnership": false,
      "isLikeAndViewCountsDisabled": false,
      "owner": {
        "id": "25025320",
        "username": "instagram",
        "fullName": "Instagram",
        "profilePicUrl": "https://instagram.com/static/images/profile.jpg",
        "isVerified": true,
        "isPrivate": false
      },
      "coowners": [],
      "location": null,
      "image": {},
      "video": null,
      "audio": null,
      "carouselMedia": []
    }
  ],
  "pagination": { "page": null, "hasNextPage": true, "cursor": "QVFE...", "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of posts returned, not the number of requests.

***

### Get User Tagged Posts

Returns posts where the Instagram user is tagged. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).

**Request**

```http
GET /instagram/user/{id}/posts/tagged
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Description                                                                    |
| ---------- | ------- | -------- | ------------------------------------------------------------------------------ |
| `id`       | string  | Yes      | Numeric Instagram user ID, 1–100 characters                                    |
| `maxItems` | integer | No       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`            |
| `cursor`   | string  | No       | Pagination cursor from a previous response. Mutually exclusive with `maxItems` |

**Example**

```bash
curl -G "https://api.pullbay.com/instagram/user/25025320/posts/tagged" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d cursor="QVFE..."
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "photo",
      "id": "3881985252256633900",
      "code": "CzXYZdef456",
      "url": "https://www.instagram.com/p/CzXYZdef456/",
      "caption": "Great day at the launch event!",
      "createdAt": 1714646400,
      "likeCount": 4300,
      "commentCount": 95,
      "isAvailable": true,
      "isCarousel": false,
      "isVideo": false,
      "isPinned": false,
      "isPaidPartnership": false,
      "isLikeAndViewCountsDisabled": false,
      "owner": {
        "id": "7894561230",
        "username": "anotheruser",
        "fullName": "Another User",
        "profilePicUrl": "https://instagram.com/static/images/profile2.jpg",
        "isVerified": false,
        "isPrivate": false
      },
      "coowners": [],
      "location": null,
      "image": {},
      "video": null,
      "audio": null,
      "carouselMedia": []
    }
  ],
  "pagination": { "page": null, "hasNextPage": true, "cursor": "QVFE...", "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of posts returned, not the number of requests.

***

### Get User Reels

Returns reels published by an Instagram user. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).

**Request**

```http
GET /instagram/user/{id}/reels
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Description                                                                    |
| ---------- | ------- | -------- | ------------------------------------------------------------------------------ |
| `id`       | string  | Yes      | Numeric Instagram user ID, 1–100 characters                                    |
| `maxItems` | integer | No       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`            |
| `cursor`   | string  | No       | Pagination cursor from a previous response. Mutually exclusive with `maxItems` |

**Example**

```bash
curl -G "https://api.pullbay.com/instagram/user/25025320/reels" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d maxItems=25
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "reel",
      "id": "3881985252256634000",
      "code": "CzREELabc789",
      "url": "https://www.instagram.com/p/CzREELabc789/",
      "caption": "New reel just dropped 🎥",
      "createdAt": 1714732800,
      "likeCount": 98000,
      "commentCount": 1820,
      "isAvailable": true,
      "isCarousel": false,
      "isVideo": true,
      "isPinned": false,
      "isPaidPartnership": false,
      "isLikeAndViewCountsDisabled": false,
      "owner": {
        "id": "25025320",
        "username": "instagram",
        "fullName": "Instagram",
        "profilePicUrl": "https://instagram.com/static/images/profile.jpg",
        "isVerified": true,
        "isPrivate": false
      },
      "coowners": [],
      "location": null,
      "image": null,
      "video": {},
      "audio": {},
      "carouselMedia": []
    }
  ],
  "pagination": { "page": null, "hasNextPage": true, "cursor": "QVFE...", "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of reels returned, not the number of requests.

***

### Get Post By Shortcode

Returns a single Instagram post by its shortcode (the segment after `/p/` in the URL).

**Request**

```http
GET /instagram/post/code/{code}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                           |
| --------- | ------ | -------- | ----------------------------------------------------- |
| `code`    | string | Yes      | Post shortcode, 1–100 characters (e.g. `CzABCdef123`) |

**Example**

```bash
curl -G "https://api.pullbay.com/instagram/post/code/CzABCdef123" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "photo",
      "id": "3881985252256633854",
      "code": "CzABCdef123",
      "url": "https://www.instagram.com/p/CzABCdef123/",
      "caption": "Check out our latest update!",
      "createdAt": 1714560000,
      "likeCount": 12500,
      "commentCount": 320,
      "isAvailable": true,
      "isCarousel": false,
      "isVideo": false,
      "isPinned": false,
      "isPaidPartnership": false,
      "isLikeAndViewCountsDisabled": false,
      "owner": {
        "id": "25025320",
        "username": "instagram",
        "fullName": "Instagram",
        "profilePicUrl": "https://instagram.com/static/images/profile.jpg",
        "isVerified": true,
        "isPrivate": false
      },
      "coowners": [],
      "location": null,
      "image": {},
      "video": null,
      "audio": null,
      "carouselMedia": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get User Stories

Returns the user's currently-active Instagram stories (non-paginated — stories expire after 24 hours, so the full set is always returned in one call).

**Request**

```http
GET /instagram/user/{id}/stories
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                 |
| --------- | ------ | -------- | ------------------------------------------- |
| `id`      | string | Yes      | Numeric Instagram user ID, 1–100 characters |

**Example**

```bash
curl -G "https://api.pullbay.com/instagram/user/25025320/stories" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "video",
      "id": "3245678901234567890",
      "code": "CzABCdef123",
      "createdAt": 1714560000,
      "expiringAt": 1714646400,
      "duration": 14.97,
      "hasAudio": true,
      "hasTranslation": false,
      "canReshare": true,
      "canReply": true,
      "isVideo": true,
      "owner": {
        "id": "25025320",
        "username": "instagram",
        "fullName": "Instagram",
        "profilePicUrl": "https://instagram.com/static/images/profile.jpg",
        "isVerified": true,
        "isPrivate": false
      },
      "coowners": [],
      "image": null,
      "video": {},
      "audio": {},
      "link": null,
      "comments": [],
      "mentions": [],
      "hashtags": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get Post Comments

Returns comments on an Instagram post. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).

**Request**

```http
GET /instagram/post/{id}/comments
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Description                                                                    |
| ---------- | ------- | -------- | ------------------------------------------------------------------------------ |
| `id`       | string  | Yes      | Numeric Instagram post ID, 1–100 characters                                    |
| `maxItems` | integer | No       | One-shot bulk pull, 1–500 results. Mutually exclusive with `cursor`            |
| `cursor`   | string  | No       | Pagination cursor from a previous response. Mutually exclusive with `maxItems` |

**Example**

```bash
curl -G "https://api.pullbay.com/instagram/post/3881985252256633854/comments" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d maxItems=100
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "comment",
      "id": "18470084392099954",
      "userId": "53847071613",
      "message": "Love this!",
      "createdAt": "2026-05-05T19:46:32.000Z",
      "likeCount": 27,
      "replyCount": 0,
      "isRanked": false,
      "user": {
        "id": "53847071613",
        "username": "someuser",
        "fullName": "Some User",
        "profilePicUrl": "https://instagram.com/static/images/profile3.jpg",
        "isPrivate": false,
        "isVerified": false
      }
    }
  ],
  "pagination": { "page": null, "hasNextPage": true, "cursor": "QVFE...", "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of comments returned, not the number of requests.

***

## Object Schemas

### User ID lookup object

| Field         | Type    | Description                                                          |
| ------------- | ------- | -------------------------------------------------------------------- |
| `isAvailable` | boolean | Whether the handle exists/is in use                                  |
| `handle`      | string  | The handle that was looked up                                        |
| `id`          | string  | Numeric Instagram user ID. Present only when `isAvailable` is `true` |

### User object

| Field             | Type    | Description                               |
| ----------------- | ------- | ----------------------------------------- |
| `type`            | string  | Always `user`                             |
| `isAvailable`     | boolean | Whether the profile is available          |
| `id`              | string  | Numeric Instagram user ID                 |
| `username`        | string  | Handle, without `@`                       |
| `fullName`        | string  | Display name                              |
| `biography`       | string  | Bio text                                  |
| `profilePicUrl`   | string  | Profile picture URL                       |
| `website`         | string  | Link in bio                               |
| `category`        | string  | Business/category label (e.g. `App page`) |
| `cityName`        | string  | City shown on profile, nullable           |
| `isPrivate`       | boolean | Whether the account is private            |
| `isVerified`      | boolean | Whether the account has a verified badge  |
| `isBusiness`      | boolean | Whether the account is a business account |
| `hasVideos`       | boolean | Whether the account has published videos  |
| `hasGuides`       | boolean | Whether the account has published guides  |
| `postCount`       | integer | Total post count                          |
| `followerCount`   | integer | Follower count                            |
| `followingCount`  | integer | Following count                           |
| `totalReelsCount` | integer | Total reel count                          |
| `bioLinks`        | array   | Links shown in the bio                    |

### Post owner object

Lightweight user reference embedded in posts and stories.

| Field           | Type    | Description                              |
| --------------- | ------- | ---------------------------------------- |
| `id`            | string  | Numeric Instagram user ID                |
| `username`      | string  | Handle, without `@`                      |
| `fullName`      | string  | Display name                             |
| `profilePicUrl` | string  | Profile picture URL                      |
| `isVerified`    | boolean | Whether the account has a verified badge |
| `isPrivate`     | boolean | Whether the account is private           |

### Post object

| Field                         | Type    | Description                                                                     |
| ----------------------------- | ------- | ------------------------------------------------------------------------------- |
| `type`                        | string  | `photo` / `video` / `carousel` / `reel`                                         |
| `id`                          | string  | Numeric post ID                                                                 |
| `code`                        | string  | Shortcode (the segment after `/p/` in the URL)                                  |
| `url`                         | string  | Permalink to the post                                                           |
| `caption`                     | string  | Caption text                                                                    |
| `createdAt`                   | integer | Unix timestamp (seconds) the post was published                                 |
| `likeCount`                   | integer | Like count                                                                      |
| `commentCount`                | integer | Comment count                                                                   |
| `isAvailable`                 | boolean | Whether the post is still available                                             |
| `isCarousel`                  | boolean | Whether the post is a multi-media carousel                                      |
| `isVideo`                     | boolean | Whether the post is a video                                                     |
| `isPinned`                    | boolean | Whether the post is pinned to the profile                                       |
| `isPaidPartnership`           | boolean | Whether the post is a paid partnership                                          |
| `isLikeAndViewCountsDisabled` | boolean | Whether like/view counts are hidden                                             |
| `owner`                       | object  | [Post owner object](#post-owner-object)                                         |
| `coowners`                    | array   | Additional tagged collaborators, each a [post owner object](#post-owner-object) |
| `location`                    | object  | Tagged location, nullable                                                       |
| `image`                       | object  | Image media data, nullable                                                      |
| `video`                       | object  | Video media data, nullable                                                      |
| `audio`                       | object  | Audio track data, nullable                                                      |
| `carouselMedia`               | array   | Per-slide media for carousel posts                                              |

### Comment object

| Field        | Type               | Description                                                                                    |
| ------------ | ------------------ | ---------------------------------------------------------------------------------------------- |
| `type`       | string             | Always `comment`                                                                               |
| `id`         | string             | Comment ID                                                                                     |
| `userId`     | string             | Numeric ID of the commenter                                                                    |
| `message`    | string             | Comment text                                                                                   |
| `createdAt`  | string (date-time) | When the comment was posted                                                                    |
| `likeCount`  | integer            | Like count                                                                                     |
| `replyCount` | integer            | Reply count                                                                                    |
| `isRanked`   | boolean            | Whether the comment is algorithmically ranked/pinned                                           |
| `user`       | object             | Commenter reference (`id`, `username`, `fullName`, `profilePicUrl`, `isPrivate`, `isVerified`) |

### Story object

| Field            | Type    | Description                                                                     |
| ---------------- | ------- | ------------------------------------------------------------------------------- |
| `type`           | string  | `photo` / `video`                                                               |
| `id`             | string  | Story ID                                                                        |
| `code`           | string  | Shortcode                                                                       |
| `createdAt`      | integer | Unix timestamp (seconds) the story was posted                                   |
| `expiringAt`     | integer | Unix timestamp (seconds) the story expires                                      |
| `duration`       | number  | Duration in seconds                                                             |
| `hasAudio`       | boolean | Whether the story has audio                                                     |
| `hasTranslation` | boolean | Whether translated text is available                                            |
| `canReshare`     | boolean | Whether the story can be reshared                                               |
| `canReply`       | boolean | Whether replies are allowed                                                     |
| `isVideo`        | boolean | Whether the story is a video                                                    |
| `owner`          | object  | [Post owner object](#post-owner-object)                                         |
| `coowners`       | array   | Additional tagged collaborators, each a [post owner object](#post-owner-object) |
| `image`          | object  | Image media data, nullable                                                      |
| `video`          | object  | Video media data, nullable                                                      |
| `audio`          | object  | Audio track data, nullable                                                      |
| `link`           | object  | Attached link sticker data, nullable                                            |
| `comments`       | array   | Comments left on the story                                                      |
| `mentions`       | array   | Mentioned accounts                                                              |
| `hashtags`       | array   | Tagged hashtags                                                                 |

***

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                  |
| ------ | ---------------------------------------- |
| `400`  | Bad request — missing/invalid parameters |
| `402`  | Insufficient credits                     |
| `404`  | User, post, or story not found           |

## Resolving a handle to a user ID

Every endpoint except the two handle-based lookups (`GET /instagram/user/handle/{handle}` and `GET /instagram/userid/{handle}`) requires a **numeric user ID**, not a handle. If you only have a handle:

1. Call `GET /instagram/userid/{handle}` to resolve it to a numeric `id` and confirm `isAvailable` is `true`.
2. Use that `id` for posts, tagged posts, reels, and stories endpoints (`/instagram/user/{id}/...`).

If you already need the full profile anyway, `GET /instagram/user/handle/{handle}` returns both the profile and the numeric `id` in one call, saving a round trip.

## Pagination

* List endpoints (posts, tagged posts, reels, comments) support **cursor-based pagination**: pass `cursor` from the previous response's `pagination.cursor` to get the next page, or use `maxItems` for a one-shot bulk pull instead. `cursor` and `maxItems` are mutually exclusive.
* `pagination.hasNextPage` indicates whether more results are available.
* The user and story endpoints (profile lookups, active stories, single post by shortcode) are not paginated — they always return the full result in one call.


# Reddit

Reddit: posts, comments, users, and subreddits. All endpoints are read-only `GET` requests and return public Reddit data — no Reddit account or OAuth app of your own is required.

**Base URL:** `https://api.pullbay.com/`

## Endpoints

| Endpoint                                             | Description                          | Pagination        |
| ---------------------------------------------------- | ------------------------------------ | ----------------- |
| `GET /reddit/search/post`                            | Search Reddit posts by keyword       | Cursor / maxItems |
| `GET /reddit/post/{id}`                              | Get a single post by ID              | —                 |
| `GET /reddit/post/{id}/comments`                     | Get the top-level comments on a post | —                 |
| `GET /reddit/post/{id}/comments/{commentId}/replies` | Get replies to a comment             | —                 |
| `GET /reddit/user/{username}`                        | Get a user profile by username       | —                 |
| `GET /reddit/user/{username}/posts`                  | Get posts submitted by a user        | Cursor / maxItems |
| `GET /reddit/user/{username}/comments`               | Get comments authored by a user      | Cursor / maxItems |
| `GET /reddit/subreddit/{name}`                       | Get a subreddit profile              | —                 |
| `GET /reddit/subreddit/{name}/posts`                 | Get posts from a subreddit           | Cursor / maxItems |

***

## Search Posts

Search Reddit posts by keyword.

**Request**

```bash
GET /reddit/search/post
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Description                                                                    |
| ---------- | ------- | -------- | ------------------------------------------------------------------------------ |
| `keyword`  | string  | Yes      | Search term, 1–100 characters                                                  |
| `sort`     | string  | No       | `relevance` (default), `hot`, `top`, `new`, or `comments`                      |
| `cursor`   | string  | No       | Pagination cursor from a previous response. Mutually exclusive with `maxItems` |
| `maxItems` | integer | No       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`            |

**Example**

```bash
curl -G "https://api.pullbay.com/reddit/search/post" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d keyword="javascript" \
  -d sort=new
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "post",
      "id": "1qclvp3",
      "title": "Is learning JavaScript still a good decision in 2026?",
      "text": "",
      "textHTML": "",
      "url": "/r/learnprogramming/comments/1qclvp3/is_learning_javascript_still_a_good_decision_in/",
      "created": 1768393294,
      "editedAt": false,
      "score": 11,
      "upvotes": 11,
      "downvotes": 0,
      "upvoteRatio": 0.68,
      "numComments": 51,
      "viewCount": null,
      "goldCount": 0,
      "numCrossposts": 0,
      "isLocked": false,
      "isStickied": false,
      "isArchived": false,
      "isOriginalContent": false,
      "isCrosspostable": false,
      "isNSFW": false,
      "isSpoiler": false,
      "isMediaOnly": false,
      "isSponsored": false,
      "isMeta": false,
      "isGildable": false,
      "isScoreHidden": false,
      "contestMode": false,
      "hidden": false,
      "sendReplies": true,
      "author": {
        "id": "t2_1w72",
        "username": "spez",
        "isBlocked": false,
        "isDeleted": false
      },
      "subreddit": {
        "id": "t5_2fwo",
        "name": "learnprogramming",
        "prefixedName": "r/learnprogramming",
        "type": "public"
      },
      "flair": null,
      "thumbnail": {},
      "preview": {},
      "awards": [],
      "flairs": [],
      "gallery": [],
      "crosspostParents": []
    }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Using `maxItems` to pull more results in one call costs proportionally more than a single page.

***

## Get Post

Returns a single Reddit post by its ID.

**Request**

```bash
GET /reddit/post/{id}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type          | Required | Description                                                     |
| --------- | ------------- | -------- | --------------------------------------------------------------- |
| `id`      | string (path) | Yes      | Post ID, 1–32 characters. Substitute directly into the URL path |

**Example**

```bash
curl -G "https://api.pullbay.com/reddit/post/1qclvp3" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "post",
      "id": "1qclvp3",
      "title": "Is learning JavaScript still a good decision in 2026?",
      "text": "",
      "textHTML": "",
      "url": "/r/learnprogramming/comments/1qclvp3/is_learning_javascript_still_a_good_decision_in/",
      "created": 1768393294,
      "editedAt": false,
      "score": 11,
      "upvotes": 11,
      "downvotes": 0,
      "upvoteRatio": 0.68,
      "numComments": 51,
      "viewCount": null,
      "goldCount": 0,
      "numCrossposts": 0,
      "isLocked": false,
      "isStickied": false,
      "isArchived": false,
      "isOriginalContent": false,
      "isCrosspostable": false,
      "isNSFW": false,
      "isSpoiler": false,
      "isMediaOnly": false,
      "isSponsored": false,
      "isMeta": false,
      "isGildable": false,
      "isScoreHidden": false,
      "contestMode": false,
      "hidden": false,
      "sendReplies": true,
      "author": {
        "id": "t2_1w72",
        "username": "spez",
        "isBlocked": false,
        "isDeleted": false
      },
      "subreddit": {
        "id": "t5_2fwo",
        "name": "learnprogramming",
        "prefixedName": "r/learnprogramming",
        "type": "public"
      },
      "flair": null,
      "thumbnail": {},
      "preview": {},
      "awards": [],
      "flairs": [],
      "gallery": [],
      "crosspostParents": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

## Comments

### Get Post Comments

Returns the top-level comment tree for a Reddit post.

**Request**

```bash
GET /reddit/post/{id}/comments
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type          | Required | Description                                                     |
| --------- | ------------- | -------- | --------------------------------------------------------------- |
| `id`      | string (path) | Yes      | Post ID, 1–32 characters. Substitute directly into the URL path |
| `sort`    | string        | No       | `best`, `top`, `new`, `controversial`, `old`, or `qa`           |

**Example**

```bash
curl -G "https://api.pullbay.com/reddit/post/1t0jood/comments" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d sort=top
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "comment",
      "id": "ojb40kw",
      "postId": "t3_1t0jood",
      "parentId": "t3_1t0jood",
      "body": "That was Rich Greenfield from LightShed.",
      "bodyHTML": "",
      "url": "/r/redditstock/comments/1t0jood/.../ojb40kw/",
      "createdAt": 1777638722,
      "editedAt": false,
      "score": 24,
      "upvotes": 24,
      "downvotes": 0,
      "goldCount": 0,
      "totalAwardsReceived": 0,
      "controversiality": 0,
      "isLocked": false,
      "isStickied": false,
      "isArchived": false,
      "isSaved": false,
      "isScoreHidden": false,
      "isCollapsed": false,
      "noFollow": false,
      "sendReplies": true,
      "hasReplies": false,
      "author": {
        "id": "t2_1w72",
        "username": "spez",
        "isBlocked": false,
        "isDeleted": false
      },
      "subreddit": {
        "id": "t5_2fwo",
        "name": "redditstock",
        "prefixedName": "r/redditstock",
        "type": "public"
      },
      "awards": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get Comment Replies

Returns replies to a specific comment on a Reddit post.

**Request**

```bash
GET /reddit/post/{id}/comments/{commentId}/replies
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter   | Type          | Required | Description                                                        |
| ----------- | ------------- | -------- | ------------------------------------------------------------------ |
| `id`        | string (path) | Yes      | Post ID, 1–32 characters. Substitute directly into the URL path    |
| `commentId` | string (path) | Yes      | Comment ID, 1–32 characters. Substitute directly into the URL path |
| `sort`      | string        | No       | `best`, `top`, `new`, `controversial`, `old`, or `qa`              |

**Example**

```bash
curl -G "https://api.pullbay.com/reddit/post/1t0jood/comments/ojb40kw/replies" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d sort=best
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "comment",
      "id": "ojb4abc",
      "postId": "t3_1t0jood",
      "parentId": "t1_ojb40kw",
      "body": "Source on that?",
      "bodyHTML": "",
      "url": "/r/redditstock/comments/1t0jood/.../ojb4abc/",
      "createdAt": 1777639022,
      "editedAt": false,
      "score": 4,
      "upvotes": 4,
      "downvotes": 0,
      "goldCount": 0,
      "totalAwardsReceived": 0,
      "controversiality": 0,
      "isLocked": false,
      "isStickied": false,
      "isArchived": false,
      "isSaved": false,
      "isScoreHidden": false,
      "isCollapsed": false,
      "noFollow": false,
      "sendReplies": true,
      "hasReplies": false,
      "author": {
        "id": "t2_9f21",
        "username": "throwaway_invest",
        "isBlocked": false,
        "isDeleted": false
      },
      "subreddit": {
        "id": "t5_2fwo",
        "name": "redditstock",
        "prefixedName": "r/redditstock",
        "type": "public"
      },
      "awards": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

## Users

### Get User

Returns a Reddit user profile by username (without `u/`).

**Request**

```bash
GET /reddit/user/{username}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type          | Required | Description                                                                                      |
| ---------- | ------------- | -------- | ------------------------------------------------------------------------------------------------ |
| `username` | string (path) | Yes      | Reddit username without the `u/` prefix, 1–100 characters. Substitute directly into the URL path |

**Example**

```bash
curl -G "https://api.pullbay.com/reddit/user/spez" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "user",
      "id": "1w72",
      "name": "spez",
      "title": "spez",
      "description": "Reddit CEO",
      "createdAt": 1118030400,
      "iconImage": "https://styles.redditmedia.com/t5_3k30p/styles/profileIcon_uj015iwx9s7g1.png",
      "snoovatarImage": "",
      "bannerImage": "https://b.thumbs.redditmedia.com/KWeEpVxXOGLoloMbM0IxGt9EiKPXizpwFgcSeWqtpZM.png",
      "awarderKarma": 0,
      "postKarma": 182496,
      "commentKarma": 755311,
      "totalKarma": 937807,
      "hasVerifiedEmail": true,
      "over18": false
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get User Posts

Returns posts submitted by a Reddit user.

**Request**

```bash
GET /reddit/user/{username}/posts
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type          | Required | Description                                                                                      |
| ---------- | ------------- | -------- | ------------------------------------------------------------------------------------------------ |
| `username` | string (path) | Yes      | Reddit username without the `u/` prefix, 1–100 characters. Substitute directly into the URL path |
| `sort`     | string        | No       | `hot`, `top`, or `new`                                                                           |
| `cursor`   | string        | No       | Pagination cursor from a previous response. Mutually exclusive with `maxItems`                   |
| `maxItems` | integer       | No       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`                              |

**Example**

```bash
curl -G "https://api.pullbay.com/reddit/user/spez/posts" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d sort=new \
  -d maxItems=50
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    { "type": "post", "id": "1qclvp3", "title": "Is learning JavaScript still a good decision in 2026?", "...": "..." }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Using `maxItems` to pull more results in one call costs proportionally more than a single page.

***

### Get User Comments

Returns comments authored by a Reddit user.

**Request**

```bash
GET /reddit/user/{username}/comments
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type          | Required | Description                                                                                      |
| ---------- | ------------- | -------- | ------------------------------------------------------------------------------------------------ |
| `username` | string (path) | Yes      | Reddit username without the `u/` prefix, 1–100 characters. Substitute directly into the URL path |
| `sort`     | string        | No       | `hot`, `top`, or `new`                                                                           |
| `cursor`   | string        | No       | Pagination cursor from a previous response. Mutually exclusive with `maxItems`                   |
| `maxItems` | integer       | No       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`                              |

**Example**

```bash
curl -G "https://api.pullbay.com/reddit/user/spez/comments" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d sort=top \
  -d cursor=eyJhZnRlciI6InQxX29qYjQwa3cifQ
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    { "type": "comment", "id": "ojb40kw", "body": "That was Rich Greenfield from LightShed.", "...": "..." }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Using `maxItems` to pull more results in one call costs proportionally more than a single page.

***

## Subreddits

### Get Subreddit

Returns a subreddit profile by its name (without `r/`).

**Request**

```bash
GET /reddit/subreddit/{name}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type          | Required | Description                                                                                     |
| --------- | ------------- | -------- | ----------------------------------------------------------------------------------------------- |
| `name`    | string (path) | Yes      | Subreddit name without the `r/` prefix, 1–100 characters. Substitute directly into the URL path |

**Example**

```bash
curl -G "https://api.pullbay.com/reddit/subreddit/programming" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "subreddit",
      "id": "2fwo",
      "name": "programming",
      "prefixedName": "r/programming",
      "title": "programming",
      "subType": "public",
      "description": "/r/programming is a reddit for discussion and news about computer programming.",
      "descriptionHtml": "",
      "publicDescription": "Computer Programming",
      "publicDescriptionHtml": "",
      "submitText": "",
      "submitTextHtml": null,
      "submissionType": "link",
      "advertiserCategory": "Technology",
      "language": "en",
      "url": "/r/programming/",
      "headerTitle": "",
      "iconImage": "",
      "iconSize": null,
      "bannerImage": "",
      "bannerBackgroundImage": "",
      "bannerSize": null,
      "headerImage": "https://b.thumbs.redditmedia.com/2rTE46grzsr-Ll3Q.png",
      "headerSize": [120, 40],
      "primaryColor": "#002b36",
      "keyColor": "",
      "createdAt": 1141150769,
      "subscribers": 6873180,
      "commentScoreHideMins": 0,
      "notificationLevel": null,
      "suggestedCommentSort": null,
      "userFlairEnabled": true,
      "linkFlairEnabled": true,
      "wikiEnabled": true,
      "emojisEnabled": false,
      "spoilersEnabled": true,
      "allowImages": false,
      "allowVideos": false,
      "allowGalleries": false,
      "allowPolls": false,
      "allowPredictions": false,
      "allowTalks": false,
      "allowDiscovery": true,
      "isCrosspostable": false,
      "publicTraffic": false,
      "showMediaPreview": true,
      "showMedia": false,
      "collapseDeletedComments": false,
      "shouldArchivePosts": false,
      "canAssignUserFlair": false,
      "canAssignLinkFlair": false,
      "restrictCommenting": false,
      "restrictPosting": true,
      "communityReviewed": true,
      "flairPosition": "right",
      "linkFlairPosition": "right"
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get Subreddit Posts

Returns posts from a subreddit.

**Request**

```bash
GET /reddit/subreddit/{name}/posts
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type          | Required | Description                                                                                     |
| ---------- | ------------- | -------- | ----------------------------------------------------------------------------------------------- |
| `name`     | string (path) | Yes      | Subreddit name without the `r/` prefix, 1–100 characters. Substitute directly into the URL path |
| `sort`     | string        | No       | `hot`, `top`, or `new`                                                                          |
| `cursor`   | string        | No       | Pagination cursor from a previous response. Mutually exclusive with `maxItems`                  |
| `maxItems` | integer       | No       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`                             |

**Example**

```bash
curl -G "https://api.pullbay.com/reddit/subreddit/programming/posts" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d sort=hot
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    { "type": "post", "id": "1qclvp3", "title": "Is learning JavaScript still a good decision in 2026?", "...": "..." }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Using `maxItems` to pull more results in one call costs proportionally more than a single page.

***

## Object schemas

### Post object

| Field                                                                                                                         | Type                   | Description                                   |
| ----------------------------------------------------------------------------------------------------------------------------- | ---------------------- | --------------------------------------------- |
| `type`                                                                                                                        | string                 | Always `post`                                 |
| `id`                                                                                                                          | string                 | Post ID                                       |
| `title`                                                                                                                       | string                 | Post title                                    |
| `text`                                                                                                                        | string                 | Self-text body (empty for link posts)         |
| `textHTML`                                                                                                                    | string                 | Rendered HTML of the self-text body           |
| `url`                                                                                                                         | string                 | Relative permalink to the post                |
| `created`                                                                                                                     | integer (unix seconds) | Submission timestamp                          |
| `editedAt`                                                                                                                    | integer or boolean     | Edit timestamp, or `false` if never edited    |
| `score`                                                                                                                       | integer                | Net score (upvotes minus downvotes)           |
| `upvotes` / `downvotes`                                                                                                       | integer                | Vote counts                                   |
| `upvoteRatio`                                                                                                                 | number                 | Fraction of votes that are upvotes            |
| `numComments`                                                                                                                 | integer                | Total comment count                           |
| `viewCount`                                                                                                                   | integer, nullable      | View count, when available                    |
| `goldCount`                                                                                                                   | integer                | Number of Reddit Gold awards                  |
| `numCrossposts`                                                                                                               | integer                | Number of crossposts of this post             |
| `isLocked` / `isStickied` / `isArchived`                                                                                      | boolean                | Moderation/state flags                        |
| `isOriginalContent` / `isCrosspostable`                                                                                       | boolean                | Content flags                                 |
| `isNSFW` / `isSpoiler` / `isMediaOnly` / `isSponsored` / `isMeta` / `isGildable` / `isScoreHidden` / `contestMode` / `hidden` | boolean                | Additional content/state flags                |
| `sendReplies`                                                                                                                 | boolean                | Whether the author allows reply notifications |
| `author`                                                                                                                      | object                 | See Author object                             |
| `subreddit`                                                                                                                   | object                 | See Subreddit Reference object                |
| `flair`                                                                                                                       | object, nullable       | Link flair, when set                          |
| `thumbnail`                                                                                                                   | object                 | Thumbnail image metadata                      |
| `preview`                                                                                                                     | object                 | Preview image/media metadata                  |
| `awards`                                                                                                                      | array                  | Awards given to the post                      |
| `flairs`                                                                                                                      | array                  | Available flair templates                     |
| `gallery`                                                                                                                     | array                  | Gallery media items, for gallery posts        |
| `crosspostParents`                                                                                                            | array                  | Parent post(s), for crossposts                |

### Comment object

| Field                                                                                  | Type                   | Description                                   |
| -------------------------------------------------------------------------------------- | ---------------------- | --------------------------------------------- |
| `type`                                                                                 | string                 | Always `comment`                              |
| `id`                                                                                   | string                 | Comment ID                                    |
| `postId`                                                                               | string                 | Fullname of the post the comment belongs to   |
| `parentId`                                                                             | string                 | Fullname of the parent (post or comment)      |
| `body`                                                                                 | string                 | Comment text                                  |
| `bodyHTML`                                                                             | string                 | Rendered HTML of the comment text             |
| `url`                                                                                  | string                 | Relative permalink to the comment             |
| `createdAt`                                                                            | integer (unix seconds) | When the comment was posted                   |
| `editedAt`                                                                             | integer or boolean     | Edit timestamp, or `false` if never edited    |
| `score`                                                                                | integer                | Net score (upvotes minus downvotes)           |
| `upvotes` / `downvotes`                                                                | integer                | Vote counts                                   |
| `goldCount`                                                                            | integer                | Number of Reddit Gold awards                  |
| `totalAwardsReceived`                                                                  | integer                | Total awards received                         |
| `controversiality`                                                                     | integer                | Reddit's controversiality flag (0 or 1)       |
| `isLocked` / `isStickied` / `isArchived` / `isSaved` / `isScoreHidden` / `isCollapsed` | boolean                | Moderation/state flags                        |
| `noFollow`                                                                             | boolean                | Whether links in the comment are `nofollow`   |
| `sendReplies`                                                                          | boolean                | Whether the author allows reply notifications |
| `hasReplies`                                                                           | boolean                | Whether this comment has reply comments       |
| `author`                                                                               | object                 | See Author object                             |
| `subreddit`                                                                            | object                 | See Subreddit Reference object                |
| `awards`                                                                               | array                  | Awards given to the comment                   |

### User object

| Field              | Type                   | Description                              |
| ------------------ | ---------------------- | ---------------------------------------- |
| `type`             | string                 | Always `user`                            |
| `id`               | string                 | User ID (without `t2_` prefix)           |
| `name`             | string                 | Username                                 |
| `title`            | string                 | Profile title (often same as username)   |
| `description`      | string                 | Profile bio/description                  |
| `createdAt`        | integer (unix seconds) | Account creation timestamp               |
| `iconImage`        | string                 | Avatar image URL                         |
| `snoovatarImage`   | string                 | Snoovatar image URL, if set              |
| `bannerImage`      | string                 | Profile banner image URL                 |
| `awarderKarma`     | integer                | Karma earned from giving awards          |
| `postKarma`        | integer                | Karma earned from posts                  |
| `commentKarma`     | integer                | Karma earned from comments               |
| `totalKarma`       | integer                | Combined karma total                     |
| `hasVerifiedEmail` | boolean                | Whether the account has a verified email |
| `over18`           | boolean                | Whether the account is marked NSFW       |

### Subreddit object

| Field                                                                                               | Type                     | Description                                           |
| --------------------------------------------------------------------------------------------------- | ------------------------ | ----------------------------------------------------- |
| `type`                                                                                              | string                   | Always `subreddit`                                    |
| `id`                                                                                                | string                   | Subreddit ID (without `t5_` prefix)                   |
| `name`                                                                                              | string                   | Subreddit name (without `r/`)                         |
| `prefixedName`                                                                                      | string                   | Subreddit name with `r/` prefix                       |
| `title`                                                                                             | string                   | Subreddit title                                       |
| `subType`                                                                                           | string                   | Subreddit visibility type (e.g. `public`)             |
| `description` / `descriptionHtml`                                                                   | string                   | Sidebar description, plain and HTML                   |
| `publicDescription` / `publicDescriptionHtml`                                                       | string                   | Short public description, plain and HTML              |
| `submitText` / `submitTextHtml`                                                                     | string, nullable         | Text shown on the submit page                         |
| `submissionType`                                                                                    | string                   | Allowed submission type (e.g. `link`, `self`, `any`)  |
| `advertiserCategory`                                                                                | string                   | Ad category classification                            |
| `language`                                                                                          | string                   | Subreddit's primary language code                     |
| `url`                                                                                               | string                   | Relative URL to the subreddit                         |
| `headerTitle`                                                                                       | string                   | Header link title                                     |
| `iconImage` / `iconSize`                                                                            | string / array, nullable | Icon image URL and dimensions                         |
| `bannerImage` / `bannerBackgroundImage` / `bannerSize`                                              | string / array, nullable | Banner images and dimensions                          |
| `headerImage` / `headerSize`                                                                        | string / array           | Header image URL and dimensions                       |
| `primaryColor` / `keyColor`                                                                         | string                   | Theme colors                                          |
| `createdAt`                                                                                         | integer (unix seconds)   | Subreddit creation timestamp                          |
| `subscribers`                                                                                       | integer                  | Subscriber count                                      |
| `commentScoreHideMins`                                                                              | integer                  | Minutes comment scores are hidden after posting       |
| `notificationLevel` / `suggestedCommentSort`                                                        | string, nullable         | User-specific notification setting and suggested sort |
| `userFlairEnabled` / `linkFlairEnabled`                                                             | boolean                  | Flair feature flags                                   |
| `wikiEnabled` / `emojisEnabled` / `spoilersEnabled`                                                 | boolean                  | Feature flags                                         |
| `allowImages` / `allowVideos` / `allowGalleries` / `allowPolls` / `allowPredictions` / `allowTalks` | boolean                  | Allowed content type flags                            |
| `allowDiscovery` / `isCrosspostable` / `publicTraffic`                                              | boolean                  | Discovery and traffic flags                           |
| `showMediaPreview` / `showMedia`                                                                    | boolean                  | Media display settings                                |
| `collapseDeletedComments` / `shouldArchivePosts`                                                    | boolean                  | Moderation settings                                   |
| `canAssignUserFlair` / `canAssignLinkFlair`                                                         | boolean                  | Flair assignment permissions                          |
| `restrictCommenting` / `restrictPosting`                                                            | boolean                  | Posting/commenting restrictions                       |
| `communityReviewed`                                                                                 | boolean                  | Whether the subreddit has passed community review     |
| `flairPosition` / `linkFlairPosition`                                                               | string                   | Flair display position (e.g. `right`)                 |

### Author object

Embedded in `Post` and `Comment` objects as the `author` field.

| Field       | Type    | Description                                             |
| ----------- | ------- | ------------------------------------------------------- |
| `id`        | string  | Author's user ID (fullname, e.g. `t2_1w72`)             |
| `username`  | string  | Author's username                                       |
| `isBlocked` | boolean | Whether the author is blocked by the requesting context |
| `isDeleted` | boolean | Whether the author's account has been deleted           |

### Subreddit Reference object

Embedded in `Post` and `Comment` objects as the `subreddit` field.

| Field          | Type   | Description                               |
| -------------- | ------ | ----------------------------------------- |
| `id`           | string | Subreddit ID (fullname, e.g. `t5_2fwo`)   |
| `name`         | string | Subreddit name (without `r/`)             |
| `prefixedName` | string | Subreddit name with `r/` prefix           |
| `type`         | string | Subreddit visibility type (e.g. `public`) |

***

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                     |
| ------ | ------------------------------------------- |
| `400`  | Bad request — missing/invalid parameters    |
| `402`  | Insufficient credits                        |
| `404`  | Post, comment, user, or subreddit not found |

## Finding Post, Comment, User, and Subreddit IDs

* **Post ID**: the alphanumeric segment in a Reddit URL, e.g. `https://reddit.com/r/learnprogramming/comments/1qclvp3/...` → `1qclvp3`.
* **Comment ID**: the alphanumeric segment after the post ID in a comment permalink, e.g. `.../comments/1t0jood/some_title/ojb40kw/` → `ojb40kw`.
* **Username**: the part after `u/` in a profile URL, e.g. `https://reddit.com/u/spez` → `spez`. Pass it without the `u/` prefix.
* **Subreddit name**: the part after `r/` in a subreddit URL, e.g. `https://reddit.com/r/programming` → `programming`. Pass it without the `r/` prefix.
* The `search/post` endpoint is the fastest way to discover post IDs for a topic without already knowing a permalink.

## Pagination Notes

* List endpoints (`search/post`, `user/{username}/posts`, `user/{username}/comments`, `subreddit/{name}/posts`) use cursor-based pagination: pass the `cursor` value from a previous response's `pagination` object to fetch the next page.
* `maxItems` is a one-shot bulk-pull alternative to `cursor` — it returns up to the requested count of results in a single call (capped at 200) instead of paging manually. `cursor` and `maxItems` are mutually exclusive on every endpoint that supports them.
* Single-resource endpoints (`post/{id}`, `post/{id}/comments`, `post/{id}/comments/{commentId}/replies`, `user/{username}`, `subreddit/{name}`) return the full result in one call and have no pagination.


# TikTok

TikTok: users, posts, comments, hashtags, and search. All endpoints are read-only `GET` requests and return public TikTok data — no account of your own is required.

**Base URL:** `https://api.pullbay.com/`

## Endpoints

| Endpoint                                             | Description                    | Pagination        |
| ---------------------------------------------------- | ------------------------------ | ----------------- |
| `GET /tiktok/user/handle/{username}`                 | Get a user by handle           | —                 |
| `GET /tiktok/user/{id}/posts`                        | Get posts published by a user  | Cursor / maxItems |
| `GET /tiktok/post/{id}`                              | Get a post by ID               | —                 |
| `GET /tiktok/post`                                   | Get a post by URL              | —                 |
| `GET /tiktok/post/{id}/comments`                     | Get comments on a post         | Cursor / maxItems |
| `GET /tiktok/post/{id}/comments/{commentId}/replies` | Get replies to a comment       | Cursor / maxItems |
| `GET /tiktok/post/search`                            | Search TikTok posts by keyword | Cursor / maxItems |
| `GET /tiktok/hashtag/handle/{name}`                  | Get a hashtag by name          | —                 |

***

## Users

### Get User by Handle

Returns a TikTok user profile by handle (without `@`).

**Request**

```http
GET /tiktok/user/handle/{username}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type   | Required | Description                                                 |
| ---------- | ------ | -------- | ----------------------------------------------------------- |
| `username` | string | Yes      | TikTok handle without `@` (e.g. `tiktok`), 1–100 characters |

**Example**

```bash
curl -G "https://api.pullbay.com/tiktok/user/handle/tiktok" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "107955",
      "username": "tiktok",
      "nickname": "TikTok",
      "url": "https://www.tiktok.com/@tiktok",
      "bio": "One TikTok can make a big impact",
      "bioUrl": "https://linktr.ee/tiktok",
      "avatar": "https://p16-common-sign.tiktokcdn.com/tos-maliva-avt-0068/avatar.jpg",
      "verified": true,
      "profileCategory": "",
      "followers": 89000000,
      "following": 7,
      "likes": 1300000000,
      "videos": 1100
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

### Get User's Posts

Returns posts published by a TikTok user. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive). `region` may bias results toward a specific country (e.g. `US`).

**Request**

```http
GET /tiktok/user/{id}/posts
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required                   | Description                                                   |
| ---------- | ------- | -------------------------- | ------------------------------------------------------------- |
| `id`       | string  | Yes                        | TikTok user ID, 1–100 characters                              |
| `region`   | string  | No                         | 2–5 character region/country code to bias results (e.g. `US`) |
| `cursor`   | string  | One of `cursor`/`maxItems` | Pagination cursor returned by a previous call                 |
| `maxItems` | integer | One of `cursor`/`maxItems` | One-shot bulk pull, 1–200 results                             |

**Example**

```bash
curl -G "https://api.pullbay.com/tiktok/user/107955/posts" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d region=US \
  -d maxItems=20
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "7636131611674692894",
      "title": "Check this out!",
      "postPage": "https://www.tiktok.com/@tiktok/video/7636131611674692894",
      "uploadedAt": 1714560000,
      "uploadedAtFormatted": "2024-05-01T00:00:00Z",
      "views": 750000,
      "likes": 12500,
      "comments": 793,
      "shares": 1250,
      "bookmarks": 320,
      "poi": null,
      "channel": {
        "id": "107955",
        "username": "tiktok",
        "name": "TikTok",
        "bio": "One TikTok can make a big impact",
        "avatar": "https://p16-common-sign.tiktokcdn.com/tos-maliva-avt-0068/avatar.jpg"
      },
      "collabInfo": {},
      "video": {},
      "song": {},
      "hashtags": [],
      "subtitleInformation": []
    }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of posts returned, not the number of requests.

***

## Posts

### Get Post by ID

Returns a single TikTok post by its numeric ID.

> **Note:** there are two ways to fetch a single post — by numeric ID (this endpoint) or by full URL (`GET /tiktok/post`, below). Use whichever you already have on hand; both return the same `TiktokPost` object.

**Request**

```http
GET /tiktok/post/{id}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                             |
| --------- | ------ | -------- | --------------------------------------- |
| `id`      | string | Yes      | Numeric TikTok post ID, 1–50 characters |
| `region`  | string | No       | 2–5 character region/country code       |

**Example**

```bash
curl -G "https://api.pullbay.com/tiktok/post/7636131611674692894" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d region=US
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "7636131611674692894",
      "title": "Check this out!",
      "postPage": "https://www.tiktok.com/@tiktok/video/7636131611674692894",
      "uploadedAt": 1714560000,
      "uploadedAtFormatted": "2024-05-01T00:00:00Z",
      "views": 750000,
      "likes": 12500,
      "comments": 793,
      "shares": 1250,
      "bookmarks": 320,
      "poi": null,
      "channel": {
        "id": "107955",
        "username": "tiktok",
        "name": "TikTok",
        "bio": "One TikTok can make a big impact",
        "avatar": "https://p16-common-sign.tiktokcdn.com/tos-maliva-avt-0068/avatar.jpg"
      },
      "collabInfo": {},
      "video": {},
      "song": {},
      "hashtags": [],
      "subtitleInformation": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

### Get Post by URL

Returns a single TikTok post by its full URL.

> **Note:** this is the second of the two ways to fetch a single post — see [Get Post by ID](#get-post-by-id) above. Handy if you scraped a link off TikTok and never extracted the numeric ID.

**Request**

```http
GET /tiktok/post
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                                                                               |
| --------- | ------ | -------- | --------------------------------------------------------------------------------------------------------- |
| `url`     | string | Yes      | Full TikTok post URL, 1–2048 characters (e.g. `https://www.tiktok.com/@tiktok/video/7636131611674692894`) |

**Example**

```bash
curl -G "https://api.pullbay.com/tiktok/post" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d url="https://www.tiktok.com/@tiktok/video/7636131611674692894"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "7636131611674692894",
      "title": "Check this out!",
      "postPage": "https://www.tiktok.com/@tiktok/video/7636131611674692894",
      "uploadedAt": 1714560000,
      "uploadedAtFormatted": "2024-05-01T00:00:00Z",
      "views": 750000,
      "likes": 12500,
      "comments": 793,
      "shares": 1250,
      "bookmarks": 320,
      "poi": null,
      "channel": {
        "id": "107955",
        "username": "tiktok",
        "name": "TikTok",
        "bio": "One TikTok can make a big impact",
        "avatar": "https://p16-common-sign.tiktokcdn.com/tos-maliva-avt-0068/avatar.jpg"
      },
      "collabInfo": {},
      "video": {},
      "song": {},
      "hashtags": [],
      "subtitleInformation": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

## Comments

### Get Post Comments

Returns comments on a TikTok post. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).

**Request**

```http
GET /tiktok/post/{id}/comments
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required                   | Description                                   |
| ---------- | ------- | -------------------------- | --------------------------------------------- |
| `id`       | string  | Yes                        | TikTok post ID, 1–100 characters              |
| `cursor`   | string  | One of `cursor`/`maxItems` | Pagination cursor returned by a previous call |
| `maxItems` | integer | One of `cursor`/`maxItems` | One-shot bulk pull, 1–500 results             |

**Example**

```bash
curl -G "https://api.pullbay.com/tiktok/post/7636131611674692894/comments" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d maxItems=50
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "7636187917325304607",
      "awemeId": "7636131611674692894",
      "text": "Love this!",
      "createdAt": "2026-05-05T19:46:32.000Z",
      "likeCount": 25,
      "replyCount": 3,
      "commentLanguage": "en",
      "isAuthorLiked": false,
      "user": {
        "id": "12345",
        "username": "someuser",
        "nickname": "Some User",
        "avatar": "https://p16-common-sign.tiktokcdn.com/tos-maliva-avt-0068/avatar2.jpg",
        "verified": false
      }
    }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of comments returned, not the number of requests.

### Get Comment Replies

Returns replies to a specific TikTok comment. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).

**Request**

```http
GET /tiktok/post/{id}/comments/{commentId}/replies
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter   | Type    | Required                   | Description                                   |
| ----------- | ------- | -------------------------- | --------------------------------------------- |
| `id`        | string  | Yes                        | TikTok post ID, 1–100 characters              |
| `commentId` | string  | Yes                        | ID of the parent comment, 1–100 characters    |
| `cursor`    | string  | One of `cursor`/`maxItems` | Pagination cursor returned by a previous call |
| `maxItems`  | integer | One of `cursor`/`maxItems` | One-shot bulk pull, 1–500 results             |

**Example**

```bash
curl -G "https://api.pullbay.com/tiktok/post/7636131611674692894/comments/7636187917325304607/replies" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d maxItems=50
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "7636187917325304608",
      "parentId": "7636187917325304607",
      "awemeId": "7636131611674692894",
      "text": "Totally agree!",
      "createdAt": "2026-05-05T20:01:12.000Z",
      "likeCount": 4,
      "commentLanguage": "en",
      "isAuthorLiked": false,
      "user": {
        "id": "67890",
        "username": "anotheruser",
        "nickname": "Another User",
        "avatar": "https://p16-common-sign.tiktokcdn.com/tos-maliva-avt-0068/avatar3.jpg",
        "verified": false
      }
    }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of replies returned, not the number of requests.

***

## Hashtags & Search

### Search Posts

Search TikTok posts by keyword. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).

**Request**

```http
GET /tiktok/post/search
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter   | Type    | Required                   | Description                                       |
| ----------- | ------- | -------------------------- | ------------------------------------------------- |
| `keyword`   | string  | Yes                        | Search term, 1–100 characters                     |
| `region`    | string  | No                         | 2–5 character region/country code to bias results |
| `dateRange` | string  | No                         | Restricts results to a date range                 |
| `sortType`  | string  | No                         | Sort order for results                            |
| `cursor`    | string  | One of `cursor`/`maxItems` | Pagination cursor returned by a previous call     |
| `maxItems`  | integer | One of `cursor`/`maxItems` | One-shot bulk pull, 1–200 results                 |

**Example**

```bash
curl -G "https://api.pullbay.com/tiktok/post/search" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d keyword="cooking hacks" \
  -d region=US \
  -d maxItems=20
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "7636131611674692894",
      "title": "Check this out!",
      "postPage": "https://www.tiktok.com/@tiktok/video/7636131611674692894",
      "uploadedAt": 1714560000,
      "uploadedAtFormatted": "2024-05-01T00:00:00Z",
      "views": 750000,
      "likes": 12500,
      "comments": 793,
      "shares": 1250,
      "bookmarks": 320,
      "poi": null,
      "channel": {
        "id": "107955",
        "username": "tiktok",
        "name": "TikTok",
        "bio": "One TikTok can make a big impact",
        "avatar": "https://p16-common-sign.tiktokcdn.com/tos-maliva-avt-0068/avatar.jpg"
      },
      "collabInfo": {},
      "video": {},
      "song": {},
      "hashtags": [],
      "subtitleInformation": []
    }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of results returned, not the number of requests.

### Get Hashtag by Name

Returns a TikTok hashtag profile by name (without `#`).

**Request**

```http
GET /tiktok/hashtag/handle/{name}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                             |
| --------- | ------ | -------- | ------------------------------------------------------- |
| `name`    | string | Yes      | Hashtag name without `#` (e.g. `fyp`), 1–100 characters |

**Example**

```bash
curl -G "https://api.pullbay.com/tiktok/hashtag/handle/fyp" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "12345",
      "name": "fyp",
      "views": 1234567890
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

## Object schemas

### User object (`TiktokUser`)

| Field             | Type    | Description                           |
| ----------------- | ------- | ------------------------------------- |
| `id`              | string  | TikTok user ID                        |
| `username`        | string  | TikTok handle (without `@`)           |
| `nickname`        | string  | Display name                          |
| `url`             | string  | Profile URL                           |
| `bio`             | string  | Bio text                              |
| `bioUrl`          | string  | Link in bio                           |
| `avatar`          | string  | Avatar image URL                      |
| `verified`        | boolean | Whether the account is verified       |
| `profileCategory` | string  | Profile category, if set              |
| `followers`       | integer | Follower count                        |
| `following`       | integer | Following count                       |
| `likes`           | integer | Total likes received across all posts |
| `videos`          | integer | Total number of posts                 |

### Post object (`TiktokPost`)

| Field                 | Type               | Description                                      |
| --------------------- | ------------------ | ------------------------------------------------ |
| `id`                  | string             | TikTok post ID                                   |
| `title`               | string             | Post caption/title                               |
| `postPage`            | string             | URL of the post                                  |
| `uploadedAt`          | integer            | Upload time, Unix timestamp in seconds           |
| `uploadedAtFormatted` | string (date-time) | Upload time, ISO 8601                            |
| `views`               | integer            | View count                                       |
| `likes`               | integer            | Like count                                       |
| `comments`            | integer            | Comment count                                    |
| `shares`              | integer            | Share count                                      |
| `bookmarks`           | integer            | Bookmark (save) count                            |
| `poi`                 | object, nullable   | Point-of-interest tag, if present                |
| `channel`             | object             | Post author — see Post Channel object below      |
| `collabInfo`          | object             | Collaborator info, if the post is a collab       |
| `video`               | object             | Video asset details (URLs, resolution, duration) |
| `song`                | object             | Sound/music details                              |
| `hashtags`            | array              | Hashtags used in the post                        |
| `subtitleInformation` | array              | Available subtitle/caption tracks                |

### Post Channel object (`TiktokPostChannel`)

Lightweight author reference embedded in `TiktokPost.channel`.

| Field      | Type   | Description                 |
| ---------- | ------ | --------------------------- |
| `id`       | string | TikTok user ID              |
| `username` | string | TikTok handle (without `@`) |
| `name`     | string | Display name                |
| `bio`      | string | Bio text                    |
| `avatar`   | string | Avatar image URL            |

### Comment object (`TiktokComment`)

Used for both top-level comments and replies.

| Field             | Type               | Description                                                         |
| ----------------- | ------------------ | ------------------------------------------------------------------- |
| `id`              | string             | Comment ID                                                          |
| `parentId`        | string             | Parent comment ID — present only on replies                         |
| `awemeId`         | string             | ID of the post the comment was made on                              |
| `text`            | string             | Comment text                                                        |
| `createdAt`       | string (date-time) | When the comment was posted                                         |
| `likeCount`       | integer            | Like count                                                          |
| `replyCount`      | integer            | Number of replies — present only on top-level comments              |
| `commentLanguage` | string             | Detected language code                                              |
| `isAuthorLiked`   | boolean            | Whether the post author liked this comment                          |
| `user`            | object             | Commenter info (`id`, `username`, `nickname`, `avatar`, `verified`) |

### Hashtag object (`TiktokHashtag`)

| Field   | Type    | Description                                         |
| ------- | ------- | --------------------------------------------------- |
| `id`    | string  | Hashtag ID                                          |
| `name`  | string  | Hashtag name (without `#`)                          |
| `views` | integer | Total view count across all posts using the hashtag |

***

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                   |
| ------ | ----------------------------------------- |
| `400`  | Bad request — missing/invalid parameters  |
| `402`  | Insufficient credits                      |
| `404`  | User, post, comment, or hashtag not found |

## Two Ways to Fetch a Post

If you already have a numeric post ID, use `GET /tiktok/post/{id}`. If you only have a TikTok link (e.g. copied from the app or scraped off the web), use `GET /tiktok/post?url=...` instead — both return the same `TiktokPost` object, so you don't need to parse the ID out of the URL yourself.

## Pagination Notes

* All four list endpoints (user posts, post comments, comment replies, post search) accept `cursor` and `maxItems` as mutually exclusive pagination strategies — pass `cursor` from a previous response to continue, or `maxItems` to pull a fixed batch in one call.
* `pagination.cursor` and `pagination.hasNextPage` in the response indicate whether more results are available.
* Single-item lookups (user by handle, post by ID/URL, hashtag by name) are never paginated.

## Finding User, Post, and Hashtag IDs

* **User ID**: returned as `id` from `GET /tiktok/user/handle/{username}`; required to fetch a user's posts.
* **Post ID**: the long numeric segment in a TikTok video URL, e.g. `https://www.tiktok.com/@tiktok/video/7636131611674692894` → `7636131611674692894`. Alternatively, pass the full URL to `GET /tiktok/post`.
* **Hashtag name**: the text after `#` in a TikTok hashtag page, without the `#` (e.g. `fyp`).


# Twitter / X

Twitter: tweets, users, handles, and search. All endpoints are read-only `GET` requests and return public Twitter/X data — no posting or account access is included.

**Base URL:** `https://api.pullbay.com/`

## Endpoints

| Endpoint                             | Description                              | Pagination        |
| ------------------------------------ | ---------------------------------------- | ----------------- |
| `GET /twitter/search`                | Search tweets by query                   | Cursor / maxItems |
| `GET /twitter/tweet/{id}`            | Get a single tweet by ID                 | —                 |
| `GET /twitter/handle/{handle}`       | Get a user by handle                     | —                 |
| `GET /twitter/handle/{handle}/about` | Get a user's about/bio details by handle | —                 |
| `GET /twitter/user/{id}`             | Get a user by numeric ID                 | —                 |
| `GET /twitter/user/{id}/tweets`      | Get tweets posted by a user              | Cursor / maxItems |

***

### Search Twitter

Search tweets matching a query.

**Request**

```bash
GET /twitter/search
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Description                                                                    |
| ---------- | ------- | -------- | ------------------------------------------------------------------------------ |
| `query`    | string  | Yes      | Search query, 1–100 characters                                                 |
| `sort`     | string  | No       | `Top` or `Latest` (default `Latest`)                                           |
| `useFirst` | string  | No       | `true` or `false` (default `false`)                                            |
| `cursor`   | string  | No       | Pagination cursor from a previous response. Mutually exclusive with `maxItems` |
| `maxItems` | integer | No       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`            |

**Example**

```bash
curl -G "https://api.pullbay.com/twitter/search" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d query="openai" \
  -d sort=Latest \
  -d maxItems=20
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "tweet",
      "id": "20",
      "url": "https://x.com/jack/status/20",
      "twitterUrl": "https://twitter.com/jack/status/20",
      "text": "just setting up my twttr",
      "fullText": "just setting up my twttr",
      "source": "Twitter Web Client",
      "lang": "en",
      "createdAt": "Tue Mar 21 20:50:14 +0000 2006",
      "conversationId": "20",
      "retweetCount": 126541,
      "replyCount": 17858,
      "likeCount": 309719,
      "quoteCount": 6384,
      "bookmarkCount": 21386,
      "viewCount": null,
      "isReply": false,
      "isPinned": false,
      "isRetweet": false,
      "isQuote": false,
      "author": {
        "type": "user",
        "id": "44196397",
        "userName": "elonmusk",
        "name": "Elon Musk",
        "url": "https://x.com/elonmusk",
        "twitterUrl": "https://twitter.com/elonmusk",
        "description": "https://t.co/dDtDyVssfm",
        "location": "",
        "profilePicture": "https://pbs.twimg.com/profile_images/2035314704307081216/71U1ftM3_normal.jpg",
        "coverPicture": "https://pbs.twimg.com/profile_banners/44196397/1774145451",
        "isVerified": true,
        "isBlueVerified": true,
        "protected": false,
        "canDm": false,
        "canMediaTag": false,
        "followers": 239698341,
        "following": 1319,
        "fastFollowersCount": 0,
        "favouritesCount": 225074,
        "listedCount": 168631,
        "mediaCount": 4467,
        "statusesCount": 101980,
        "createdAt": "Tue Jun 02 20:12:29 +0000 2009",
        "status": ""
      },
      "media": []
    }
  ],
  "pagination": { "page": null, "hasNextPage": true, "cursor": "DAACCgACGY...", "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Using `maxItems` to pull more results in one call costs proportionally more than a single page.

***

### Get Tweet

Returns a single tweet by its numeric ID.

**Request**

```bash
GET /twitter/tweet/{id}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                    |
| --------- | ------ | -------- | ---------------------------------------------- |
| `id`      | string | Yes      | Numeric tweet ID, 1–100 characters (e.g. `20`) |

**Example**

```bash
curl -G "https://api.pullbay.com/twitter/tweet/20" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "tweet",
      "id": "20",
      "url": "https://x.com/jack/status/20",
      "twitterUrl": "https://twitter.com/jack/status/20",
      "text": "just setting up my twttr",
      "fullText": "just setting up my twttr",
      "source": "Twitter Web Client",
      "lang": "en",
      "createdAt": "Tue Mar 21 20:50:14 +0000 2006",
      "conversationId": "20",
      "retweetCount": 126541,
      "replyCount": 17858,
      "likeCount": 309719,
      "quoteCount": 6384,
      "bookmarkCount": 21386,
      "viewCount": null,
      "isReply": false,
      "isPinned": false,
      "isRetweet": false,
      "isQuote": false,
      "author": {
        "type": "user",
        "id": "12",
        "userName": "jack",
        "name": "jack",
        "url": "https://x.com/jack",
        "twitterUrl": "https://twitter.com/jack",
        "description": "",
        "location": "",
        "profilePicture": "https://pbs.twimg.com/profile_images/1115644092329758721/AFjOzXz3_normal.jpg",
        "coverPicture": "",
        "isVerified": false,
        "isBlueVerified": false,
        "protected": false,
        "canDm": false,
        "canMediaTag": false,
        "followers": 6800000,
        "following": 4500,
        "fastFollowersCount": 0,
        "favouritesCount": 25000,
        "listedCount": 22000,
        "mediaCount": 350,
        "statusesCount": 30000,
        "createdAt": "Tue Mar 21 20:50:14 +0000 2006",
        "status": ""
      },
      "media": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get User By Handle

Returns a Twitter user by handle (without `@`).

**Request**

```bash
GET /twitter/handle/{handle}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                                    |
| --------- | ------ | -------- | -------------------------------------------------------------- |
| `handle`  | string | Yes      | Twitter handle without `@`, 1–100 characters (e.g. `elonmusk`) |

**Example**

```bash
curl -G "https://api.pullbay.com/twitter/handle/elonmusk" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "user",
      "id": "44196397",
      "userName": "elonmusk",
      "name": "Elon Musk",
      "url": "https://x.com/elonmusk",
      "twitterUrl": "https://twitter.com/elonmusk",
      "description": "https://t.co/dDtDyVssfm",
      "location": "",
      "profilePicture": "https://pbs.twimg.com/profile_images/2035314704307081216/71U1ftM3_normal.jpg",
      "coverPicture": "https://pbs.twimg.com/profile_banners/44196397/1774145451",
      "isVerified": true,
      "isBlueVerified": true,
      "protected": false,
      "canDm": false,
      "canMediaTag": false,
      "followers": 239698341,
      "following": 1319,
      "fastFollowersCount": 0,
      "favouritesCount": 225074,
      "listedCount": 168631,
      "mediaCount": 4467,
      "statusesCount": 101980,
      "createdAt": "Tue Jun 02 20:12:29 +0000 2009",
      "status": ""
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get User About By Handle

Returns extended profile / about details for a Twitter user, by handle.

**Request**

```bash
GET /twitter/handle/{handle}/about
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                                    |
| --------- | ------ | -------- | -------------------------------------------------------------- |
| `handle`  | string | Yes      | Twitter handle without `@`, 1–100 characters (e.g. `elonmusk`) |

**Example**

```bash
curl -G "https://api.pullbay.com/twitter/handle/elonmusk/about" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "accountBasedIn": "United States",
      "locationAccurate": true,
      "profileImageShape": "Circle",
      "avatarUrl": "https://pbs.twimg.com/profile_images/2035314704307081216/71U1ftM3_normal.jpg",
      "usernameChangeCount": "0",
      "accountCreatedAt": "Tue Jun 02 20:12:29 +0000 2009",
      "accountSource": "United States App Store",
      "verifiedSince": "-156836000000000"
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get User By ID

Returns a Twitter user by their numeric user ID.

**Request**

```bash
GET /twitter/user/{id}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                                 |
| --------- | ------ | -------- | ----------------------------------------------------------- |
| `id`      | string | Yes      | Numeric Twitter user ID, 1–100 characters (e.g. `44196397`) |

**Example**

```bash
curl -G "https://api.pullbay.com/twitter/user/44196397" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "user",
      "id": "44196397",
      "userName": "elonmusk",
      "name": "Elon Musk",
      "url": "https://x.com/elonmusk",
      "twitterUrl": "https://twitter.com/elonmusk",
      "description": "https://t.co/dDtDyVssfm",
      "location": "",
      "profilePicture": "https://pbs.twimg.com/profile_images/2035314704307081216/71U1ftM3_normal.jpg",
      "coverPicture": "https://pbs.twimg.com/profile_banners/44196397/1774145451",
      "isVerified": true,
      "isBlueVerified": true,
      "protected": false,
      "canDm": false,
      "canMediaTag": false,
      "followers": 239698341,
      "following": 1319,
      "fastFollowersCount": 0,
      "favouritesCount": 225074,
      "listedCount": 168631,
      "mediaCount": 4467,
      "statusesCount": 101980,
      "createdAt": "Tue Jun 02 20:12:29 +0000 2009",
      "status": ""
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get User's Tweets

Returns tweets posted by a user, by user ID.

**Request**

```bash
GET /twitter/user/{id}/tweets
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Description                                                                    |
| ---------- | ------- | -------- | ------------------------------------------------------------------------------ |
| `id`       | string  | Yes      | Numeric Twitter user ID, 1–100 characters (e.g. `44196397`)                    |
| `cursor`   | string  | No       | Pagination cursor from a previous response. Mutually exclusive with `maxItems` |
| `maxItems` | integer | No       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`            |

**Example**

```bash
curl -G "https://api.pullbay.com/twitter/user/44196397/tweets" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d maxItems=20
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "tweet",
      "id": "20",
      "url": "https://x.com/jack/status/20",
      "twitterUrl": "https://twitter.com/jack/status/20",
      "text": "just setting up my twttr",
      "fullText": "just setting up my twttr",
      "source": "Twitter Web Client",
      "lang": "en",
      "createdAt": "Tue Mar 21 20:50:14 +0000 2006",
      "conversationId": "20",
      "retweetCount": 126541,
      "replyCount": 17858,
      "likeCount": 309719,
      "quoteCount": 6384,
      "bookmarkCount": 21386,
      "viewCount": null,
      "isReply": false,
      "isPinned": false,
      "isRetweet": false,
      "isQuote": false,
      "author": {
        "type": "user",
        "id": "44196397",
        "userName": "elonmusk",
        "name": "Elon Musk",
        "url": "https://x.com/elonmusk",
        "twitterUrl": "https://twitter.com/elonmusk",
        "description": "https://t.co/dDtDyVssfm",
        "location": "",
        "profilePicture": "https://pbs.twimg.com/profile_images/2035314704307081216/71U1ftM3_normal.jpg",
        "coverPicture": "https://pbs.twimg.com/profile_banners/44196397/1774145451",
        "isVerified": true,
        "isBlueVerified": true,
        "protected": false,
        "canDm": false,
        "canMediaTag": false,
        "followers": 239698341,
        "following": 1319,
        "fastFollowersCount": 0,
        "favouritesCount": 225074,
        "listedCount": 168631,
        "mediaCount": 4467,
        "statusesCount": 101980,
        "createdAt": "Tue Jun 02 20:12:29 +0000 2009",
        "status": ""
      },
      "media": []
    }
  ],
  "pagination": { "page": null, "hasNextPage": true, "cursor": "DAACCgACGY...", "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of tweets returned, not the number of requests.

***

## Object schemas

### User object

| Field                             | Type    | Description                                           |
| --------------------------------- | ------- | ----------------------------------------------------- |
| `type`                            | string  | Always `user`                                         |
| `id`                              | string  | Numeric Twitter user ID                               |
| `userName`                        | string  | Handle, without `@`                                   |
| `name`                            | string  | Display name                                          |
| `url` / `twitterUrl`              | string  | Profile URL on `x.com` and `twitter.com`              |
| `description`                     | string  | Bio text                                              |
| `location`                        | string  | Free-text profile location                            |
| `profilePicture` / `coverPicture` | string  | Avatar and banner image URLs                          |
| `isVerified` / `isBlueVerified`   | boolean | Legacy verification and Blue subscriber verification  |
| `protected`                       | boolean | Whether the account's tweets are protected            |
| `canDm` / `canMediaTag`           | boolean | Whether the user accepts DMs / can be tagged in media |
| `followers` / `following`         | integer | Follower and following counts                         |
| `fastFollowersCount`              | integer | Recently-gained follower count                        |
| `favouritesCount`                 | integer | Total likes given by the user                         |
| `listedCount`                     | integer | Number of lists the user appears on                   |
| `mediaCount`                      | integer | Number of tweets containing media                     |
| `statusesCount`                   | integer | Total tweet count                                     |
| `createdAt`                       | string  | Account creation timestamp (Twitter date format)      |
| `status`                          | string  | Reserved/internal status field                        |

### Tweet object

| Field                                                                        | Type                        | Description                                  |
| ---------------------------------------------------------------------------- | --------------------------- | -------------------------------------------- |
| `type`                                                                       | string                      | Always `tweet`                               |
| `id`                                                                         | string                      | Numeric tweet ID                             |
| `url` / `twitterUrl`                                                         | string                      | Tweet permalink on `x.com` and `twitter.com` |
| `text` / `fullText`                                                          | string                      | Tweet body (truncated and full)              |
| `source`                                                                     | string                      | Client used to post the tweet                |
| `lang`                                                                       | string                      | Tweet language code                          |
| `createdAt`                                                                  | string                      | Posted timestamp (Twitter date format)       |
| `conversationId`                                                             | string                      | ID of the root tweet in the thread           |
| `retweetCount` / `replyCount` / `likeCount` / `quoteCount` / `bookmarkCount` | integer                     | Engagement counts                            |
| `viewCount`                                                                  | integer, nullable           | View count, when available                   |
| `isReply` / `isPinned` / `isRetweet` / `isQuote`                             | boolean                     | Tweet type flags                             |
| `author`                                                                     | [User object](#user-object) | The tweet's author                           |
| `media`                                                                      | array                       | Attached media objects                       |

### User about object

| Field                 | Type    | Description                                      |
| --------------------- | ------- | ------------------------------------------------ |
| `accountBasedIn`      | string  | Inferred account region                          |
| `locationAccurate`    | boolean | Whether `accountBasedIn` is considered accurate  |
| `profileImageShape`   | string  | Profile picture mask shape (e.g. `Circle`)       |
| `avatarUrl`           | string  | Avatar image URL                                 |
| `usernameChangeCount` | string  | Number of times the handle has been changed      |
| `accountCreatedAt`    | string  | Account creation timestamp (Twitter date format) |
| `accountSource`       | string  | Platform/channel the account was created from    |
| `verifiedSince`       | string  | Verification timestamp, when applicable          |

***

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                  |
| ------ | ---------------------------------------- |
| `400`  | Bad request — missing/invalid parameters |
| `402`  | Insufficient credits                     |
| `404`  | Tweet or user not found                  |

## Looking Up Users: Handle vs. ID

Twitter users can be resolved two ways, and the right one depends on what you already have:

* **By handle** (`/twitter/handle/{handle}`, `/twitter/handle/{handle}/about`) — use when you have the `@username` (without the `@`). This is the natural entry point when a human gives you a Twitter handle.
* **By numeric ID** (`/twitter/user/{id}`, `/twitter/user/{id}/tweets`) — use when you already have the user's numeric ID, typically because you pulled it from `author.id` on a tweet or from a prior handle lookup's `id` field. Numeric IDs are stable even if a user changes their handle, so prefer caching the ID for repeated lookups.

This mirrors the Instagram service's handle-vs-ID split: resolve once by handle to get the numeric ID, then use the ID for subsequent lookups (like fetching a user's tweets) to avoid re-resolving the handle each time.

Profile detail also comes in two levels:

* **Basic profile** (`/twitter/handle/{handle}`, `/twitter/user/{id}`) — followers, bio, verification status, and engagement counters. Use this for most lookups.
* **About / extended profile** (`/twitter/handle/{handle}/about`) — additional metadata not on the basic profile, such as inferred account region, profile image shape, username change count, and verification timestamp. Only available via handle, not by numeric ID. Call this in addition to the basic profile when you need these extra fields.


# Yelp

Yelp: places, reviews, and search. All endpoints are read-only `GET` requests and return public Yelp data — no Yelp account of your own is required.

**Base URL:** `https://api.pullbay.com/`

## Endpoints

| Endpoint                          | Description             | Pagination      |
| --------------------------------- | ----------------------- | --------------- |
| `GET /yelp/place/{id}`            | Get a place by ID       | —               |
| `GET /yelp/place/{id}/reviews`    | Get reviews for a place | Page / maxItems |
| `GET /yelp/place/handle/{handle}` | Get a place by handle   | —               |
| `GET /yelp/search`                | Search Yelp places      | Page / maxItems |

***

### Get Place

Returns a single Yelp place by its Yelp business ID.

**Request**

```http
GET /yelp/place/{id}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                                        |
| --------- | ------ | -------- | ------------------------------------------------------------------ |
| `id`      | string | Yes      | Yelp business ID, 1–200 characters (e.g. `JQNDglh1GHuvHriHAcJ1yg`) |

**Example**

```bash
curl -G "https://api.pullbay.com/yelp/place/JQNDglh1GHuvHriHAcJ1yg" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "JQNDglh1GHuvHriHAcJ1yg",
      "alias": "starbucks-seattle-121",
      "title": "Starbucks",
      "website": "https://www.starbucks.com/",
      "phoneNumber": "+12065551212",
      "avgRating": 3.5,
      "unroundedAvgRating": 3.4,
      "reviewCount": 105,
      "priceRange": "$$",
      "isBusinessClosed": false,
      "primaryPhoto": "https://s3-media0.fl.yelpcdn.com/bphoto/abc123/o.jpg",
      "mediaCount": 42,
      "address": { "...": "..." },
      "coordinates": { "latitude": 47.6062, "longitude": -122.3321 },
      "categories": [{ "...": "..." }],
      "operationHours": [{ "...": "..." }],
      "amenities": [{ "...": "..." }],
      "about": { "...": "..." }
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get Place Reviews

Returns reviews for a Yelp place. Use `page` (1-indexed) to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).

**Request**

```http
GET /yelp/place/{id}/reviews
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Description                                                       |
| ---------- | ------- | -------- | ----------------------------------------------------------------- |
| `id`       | string  | Yes      | Yelp business ID, 1–200 characters                                |
| `sortBy`   | string  | No       | Sort order for reviews, 1–50 characters                           |
| `page`     | integer | No       | Page number, 1–100. Mutually exclusive with `maxItems`            |
| `maxItems` | integer | No       | One-shot bulk pull, 1–200 results. Mutually exclusive with `page` |

**Example**

```bash
curl -G "https://api.pullbay.com/yelp/place/JQNDglh1GHuvHriHAcJ1yg/reviews" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d sortBy=relevance_desc \
  -d page=1
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "_pAYrXvfEr5MPj4eBAfjmA",
      "url": "https://www.yelp.com/biz/starbucks-seattle-121?hrid=_pAYrXvfEr5MPj4eBAfjmA",
      "rating": 5,
      "text": "Great coffee and service!",
      "language": "en",
      "date": "2026-04-30T10:00:00.000Z",
      "images": [],
      "feedback": { "...": "..." },
      "reviewer": {
        "id": "abc123",
        "name": "Jane D.",
        "profileUrl": "https://www.yelp.com/user_details?userid=abc123",
        "image": "https://s3-media0.fl.yelpcdn.com/photo/abc123/o.jpg"
      }
    }
  ],
  "pagination": { "page": 1, "hasNextPage": true, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of reviews returned, not the number of requests.

***

### Get Place By Handle

Returns a Yelp place by its handle / slug (the vanity URL segment used on yelp.com).

**Request**

```http
GET /yelp/place/handle/{handle}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                                          |
| --------- | ------ | -------- | -------------------------------------------------------------------- |
| `handle`  | string | Yes      | Place handle / slug, 1–200 characters (e.g. `starbucks-seattle-121`) |

**Example**

```bash
curl -G "https://api.pullbay.com/yelp/place/handle/starbucks-seattle-121" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "JQNDglh1GHuvHriHAcJ1yg",
      "alias": "starbucks-seattle-121",
      "title": "Starbucks",
      "website": "https://www.starbucks.com/",
      "phoneNumber": "+12065551212",
      "avgRating": 3.5,
      "unroundedAvgRating": 3.4,
      "reviewCount": 105,
      "priceRange": "$$",
      "isBusinessClosed": false,
      "primaryPhoto": "https://s3-media0.fl.yelpcdn.com/bphoto/abc123/o.jpg",
      "mediaCount": 42,
      "address": { "...": "..." },
      "coordinates": { "latitude": 47.6062, "longitude": -122.3321 },
      "categories": [{ "...": "..." }],
      "operationHours": [{ "...": "..." }],
      "amenities": [{ "...": "..." }],
      "about": { "...": "..." }
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Search Places

Search Yelp places by query and location. Use `page` (1-indexed) to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).

**Request**

```http
GET /yelp/search
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Description                                                       |
| ---------- | ------- | -------- | ----------------------------------------------------------------- |
| `query`    | string  | Yes      | Search term, 1–200 characters (e.g. `coffee`)                     |
| `location` | string  | Yes      | Location to search near, 1–200 characters (e.g. `Seattle, WA`)    |
| `sortBy`   | string  | No       | Sort order for results, 1–50 characters                           |
| `page`     | integer | No       | Page number, 1–100. Mutually exclusive with `maxItems`            |
| `maxItems` | integer | No       | One-shot bulk pull, 1–200 results. Mutually exclusive with `page` |

**Example**

```bash
curl -G "https://api.pullbay.com/yelp/search" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d query=coffee \
  -d location="Seattle, WA" \
  -d page=1
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "JQNDglh1GHuvHriHAcJ1yg",
      "alias": "starbucks-seattle-121",
      "name": "Starbucks",
      "rating": 3.5,
      "reviewCount": 105,
      "priceRange": "$$",
      "phone": "+12065551212",
      "website": "https://www.starbucks.com/",
      "coordinates": { "latitude": 47.6062, "longitude": -122.3321 },
      "categories": [{ "...": "..." }],
      "images": ["https://s3-media0.fl.yelpcdn.com/bphoto/abc123/o.jpg"]
    }
  ],
  "pagination": { "page": 1, "hasNextPage": true, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Using `maxItems` to pull more results in one call costs proportionally more than a single page.

***

## Object schemas

### Place object

| Field                | Type    | Description                                 |
| -------------------- | ------- | ------------------------------------------- |
| `id`                 | string  | Yelp business ID                            |
| `alias`              | string  | Handle / slug used in the place's Yelp URL  |
| `title`              | string  | Place name                                  |
| `website`            | string  | Place's external website                    |
| `phoneNumber`        | string  | Contact phone number                        |
| `avgRating`          | number  | Rounded average rating                      |
| `unroundedAvgRating` | number  | Unrounded average rating                    |
| `reviewCount`        | integer | Total number of reviews                     |
| `priceRange`         | string  | Price tier (e.g. `$$`)                      |
| `isBusinessClosed`   | boolean | Whether the business is permanently closed  |
| `primaryPhoto`       | string  | URL of the primary photo                    |
| `mediaCount`         | integer | Number of photos/media items on the profile |
| `address`            | object  | Structured address fields                   |
| `coordinates`        | object  | `latitude` / `longitude`                    |
| `categories`         | array   | Business categories                         |
| `operationHours`     | array   | Hours of operation per day                  |
| `amenities`          | array   | Amenities offered (e.g. takeout, delivery)  |
| `about`              | object  | "About this business" details               |

### Review object

| Field      | Type               | Description                                         |
| ---------- | ------------------ | --------------------------------------------------- |
| `id`       | string             | Review ID                                           |
| `url`      | string             | Permalink to the review                             |
| `rating`   | number             | Rating, 1–5                                         |
| `text`     | string             | Review body                                         |
| `language` | string             | Language code of the review text                    |
| `date`     | string (date-time) | When the review was posted                          |
| `images`   | array              | Image URLs attached to the review                   |
| `feedback` | object             | Useful/funny/cool feedback counts                   |
| `reviewer` | object             | `id`, `name`, `profileUrl`, `image` of the reviewer |

### Search result object

| Field         | Type           | Description                                |
| ------------- | -------------- | ------------------------------------------ |
| `id`          | string         | Yelp business ID                           |
| `alias`       | string         | Handle / slug used in the place's Yelp URL |
| `name`        | string         | Place name                                 |
| `rating`      | number         | Average rating                             |
| `reviewCount` | integer        | Total number of reviews                    |
| `priceRange`  | string         | Price tier (e.g. `$$`)                     |
| `phone`       | string \| null | Contact phone number                       |
| `website`     | string \| null | Place's external website                   |
| `coordinates` | object         | `latitude` / `longitude`                   |
| `categories`  | array          | Business categories                        |
| `images`      | array          | Photo URLs                                 |

Note: the search result object is a lighter-weight summary than the full place object — it uses `name` instead of `title`, `phone` instead of `phoneNumber`, and omits `address`, `operationHours`, `amenities`, `about`, `isBusinessClosed`, `primaryPhoto`, and `mediaCount`. Fetch the full place by `id` or `alias` (handle) if you need those fields.

***

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                  |
| ------ | ---------------------------------------- |
| `400`  | Bad request — missing/invalid parameters |
| `402`  | Insufficient credits                     |
| `404`  | Place or review set not found            |

## Finding Place IDs and Handles

* **Place ID**: the opaque ID Yelp assigns to a business, e.g. `JQNDglh1GHuvHriHAcJ1yg`. You'll typically get this from a prior `/yelp/search` call's `id` field.
* **Handle**: the vanity slug in a place's Yelp URL, e.g. `https://www.yelp.com/biz/starbucks-seattle-121` → `starbucks-seattle-121`. Search results also return this as `alias`.
* Both resolve the same place — use `GET /yelp/place/{id}` if you already have the ID (from a search result or a previous lookup), or `GET /yelp/place/handle/{handle}` if you only have a Yelp URL or slug.

## Pagination

* `page` and `maxItems` are mutually exclusive on both `/yelp/search` and `/yelp/place/{id}/reviews`.
* `page` returns one page at a time; check `pagination.hasNextPage` to know whether to request the next page.
* `maxItems` pulls up to the requested count in a single call instead of paging manually — useful for one-shot bulk pulls, but billed proportionally to the number of results (see each endpoint's credit cost note above).


# Youtube

YouTube: channels, videos, shorts, live streams, comments, search, trending, and hashtags. All endpoints are read-only `GET` requests and return public YouTube data — no channel or video of your own is required.

**Base URL:** `https://api.pullbay.com/`

## Endpoints

| Endpoint                                 | Description                                       | Pagination        |
| ---------------------------------------- | ------------------------------------------------- | ----------------- |
| `GET /youtube/channel`                   | Get a single channel by ID, handle, or URL        | —                 |
| `GET /youtube/channel/{id}/videos`       | Get videos uploaded by a channel                  | Cursor / maxItems |
| `GET /youtube/channel/{id}/shorts`       | Get shorts uploaded by a channel                  | Cursor / maxItems |
| `GET /youtube/channel/{id}/live-streams` | Get live streams (current and past) for a channel | Cursor / maxItems |
| `GET /youtube/video`                     | Get a single video's full metadata                | —                 |
| `GET /youtube/shorts/{id}`               | Get a single short's full metadata                | —                 |
| `GET /youtube/comments`                  | Get top-level comments on a video                 | Cursor / maxItems |
| `GET /youtube/search`                    | Search videos, channels, or playlists             | Cursor / maxItems |
| `GET /youtube/hashtag`                   | Get videos tagged with a hashtag                  | Cursor / maxItems |
| `GET /youtube/trending`                  | Get currently trending videos                     | —                 |

***

### Get Channel

Returns channel details. Provide exactly one of `id`, `handle`, or `channelUrl`.

**Request**

```bash
GET /youtube/channel
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter    | Type   | Required                          | Description                                                    |
| ------------ | ------ | --------------------------------- | -------------------------------------------------------------- |
| `id`         | string | One of `id`/`handle`/`channelUrl` | Channel ID, 1–100 characters (e.g. `UCX6OQ3DkcsbYNE6H8uQQuVA`) |
| `handle`     | string | One of `id`/`handle`/`channelUrl` | Channel handle, 1–100 characters (e.g. `@MrBeast`)             |
| `channelUrl` | string | One of `id`/`handle`/`channelUrl` | Full URL to the channel, 10–1000 characters                    |

**Example**

```bash
curl -G "https://api.pullbay.com/youtube/channel" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d handle=@MrBeast
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "channel",
      "id": "UCX6OQ3DkcsbYNE6H8uQQuVA",
      "name": "MrBeast",
      "handle": "@MrBeast",
      "url": "http://www.youtube.com/@MrBeast",
      "description": "SUBSCRIBE FOR A COOKIE!",
      "thumbnails": "https://yt3.googleusercontent.com/nxYrc_1_2f77DoBadyxMTmv7ZpRZapHR5jbuYe7PlPd5cIRJxtNNEYyOC0ZsxaDyJJzXrnJiuDE",
      "isFamilySafe": true,
      "subscriberCount": "421M subscribers",
      "availableCountries": ["US", "GB", "CA"],
      "tags": ["mrbeast", "beast"],
      "joinedAt": "Joined Feb 20, 2012",
      "viewCount": "87,234,567,890 views",
      "videoCount": 837
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get Channel Videos

Returns videos uploaded by the channel. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).

**Request**

```bash
GET /youtube/channel/{id}/videos
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Description                                                                           |
| ---------- | ------- | -------- | ------------------------------------------------------------------------------------- |
| `id`       | string  | Yes      | Channel ID, 1–100 characters — passed in the URL path                                 |
| `cursor`   | string  | No       | Opaque pagination cursor from a previous response. Mutually exclusive with `maxItems` |
| `maxItems` | integer | No       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`                   |

**Example**

```bash
curl -G "https://api.pullbay.com/youtube/channel/UCX6OQ3DkcsbYNE6H8uQQuVA/videos" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d maxItems=25
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "video",
      "id": "EerdGm-ehJQ",
      "url": "https://www.youtube.com/watch?v=EerdGm-ehJQ",
      "title": "JavaScript Tutorial Full Course - Beginner to Pro",
      "description": "Get a certificate for this course",
      "thumbnails": [
        { "url": "https://i.ytimg.com/vi/EerdGm-ehJQ/hqdefault.jpg", "width": 480, "height": 360 }
      ],
      "richThumbnail": [],
      "channel": {
        "id": "UCX6OQ3DkcsbYNE6H8uQQuVA",
        "name": "MrBeast",
        "handle": "@MrBeast",
        "url": "https://www.youtube.com/channel/UCX6OQ3DkcsbYNE6H8uQQuVA",
        "subscriberCount": "421M subscribers",
        "badges": [],
        "thumbnails": []
      },
      "publishDate": "1 year ago",
      "views": "8531468",
      "duration": "22:15:57"
    }
  ],
  "pagination": { "page": null, "hasNextPage": true, "cursor": "CAUQAA", "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of videos returned, not the number of requests.

***

### Get Channel Shorts

Returns shorts uploaded by the channel. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).

**Request**

```bash
GET /youtube/channel/{id}/shorts
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Description                                                                           |
| ---------- | ------- | -------- | ------------------------------------------------------------------------------------- |
| `id`       | string  | Yes      | Channel ID, 1–100 characters — passed in the URL path                                 |
| `cursor`   | string  | No       | Opaque pagination cursor from a previous response. Mutually exclusive with `maxItems` |
| `maxItems` | integer | No       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`                   |

**Example**

```bash
curl -G "https://api.pullbay.com/youtube/channel/UCX6OQ3DkcsbYNE6H8uQQuVA/shorts" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d maxItems=25
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "video",
      "id": "kJQP7kiw5Fk",
      "url": "https://www.youtube.com/watch?v=kJQP7kiw5Fk",
      "title": "I Gave My 1,000th Subscriber A Island",
      "description": "Shorts edit of the full video",
      "thumbnails": [
        { "url": "https://i.ytimg.com/vi/kJQP7kiw5Fk/hqdefault.jpg", "width": 480, "height": 360 }
      ],
      "richThumbnail": [],
      "channel": {
        "id": "UCX6OQ3DkcsbYNE6H8uQQuVA",
        "name": "MrBeast",
        "handle": "@MrBeast",
        "url": "https://www.youtube.com/channel/UCX6OQ3DkcsbYNE6H8uQQuVA",
        "subscriberCount": "421M subscribers",
        "badges": [],
        "thumbnails": []
      },
      "publishDate": "3 weeks ago",
      "views": "42531468",
      "duration": "0:58"
    }
  ],
  "pagination": { "page": null, "hasNextPage": true, "cursor": "CAUQAA", "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of shorts returned, not the number of requests.

***

### Get Channel Live Streams

Returns live streams (current and past) for the channel. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).

**Request**

```bash
GET /youtube/channel/{id}/live-streams
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Description                                                                           |
| ---------- | ------- | -------- | ------------------------------------------------------------------------------------- |
| `id`       | string  | Yes      | Channel ID, 1–100 characters — passed in the URL path                                 |
| `cursor`   | string  | No       | Opaque pagination cursor from a previous response. Mutually exclusive with `maxItems` |
| `maxItems` | integer | No       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`                   |

**Example**

```bash
curl -G "https://api.pullbay.com/youtube/channel/UCX6OQ3DkcsbYNE6H8uQQuVA/live-streams" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d maxItems=25
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "video",
      "id": "5qap5aO4i9A",
      "url": "https://www.youtube.com/watch?v=5qap5aO4i9A",
      "title": "lofi hip hop radio - beats to relax/study to",
      "description": "24/7 livestream",
      "thumbnails": [
        { "url": "https://i.ytimg.com/vi/5qap5aO4i9A/hqdefault.jpg", "width": 480, "height": 360 }
      ],
      "richThumbnail": [],
      "channel": {
        "id": "UCX6OQ3DkcsbYNE6H8uQQuVA",
        "name": "MrBeast",
        "handle": "@MrBeast",
        "url": "https://www.youtube.com/channel/UCX6OQ3DkcsbYNE6H8uQQuVA",
        "subscriberCount": "421M subscribers",
        "badges": [],
        "thumbnails": []
      },
      "publishDate": "Streamed live",
      "views": "9823145",
      "duration": "LIVE"
    }
  ],
  "pagination": { "page": null, "hasNextPage": true, "cursor": "CAUQAA", "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of streams returned, not the number of requests.

***

### Get Video

Returns full metadata for a single video by its 11-character video `id`.

**Request**

```bash
GET /youtube/video
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                                 |
| --------- | ------ | -------- | ----------------------------------------------------------- |
| `id`      | string | Yes      | 11-character video ID, 5–20 characters (e.g. `dQw4w9WgXcQ`) |
| `type`    | string | No       | `video` or `shorts` — hints the lookup at the content type  |

**Example**

```bash
curl -G "https://api.pullbay.com/youtube/video" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d id=dQw4w9WgXcQ \
  -d type=video
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "video",
      "id": "dQw4w9WgXcQ",
      "title": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)",
      "status": "OK",
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "description": "The official video for \"Never Gonna Give You Up\" by Rick Astley.",
      "duration": 213,
      "views": 1766758607,
      "likes": 18978439,
      "comments": 2400000,
      "channel": {
        "id": "UCuAXFkgsw1L7xaCfnd5JJOw",
        "name": "Rick Astley",
        "handle": "@RickAstleyYT",
        "url": "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw",
        "subscriberCount": "4.49M subscribers",
        "badges": ["Official Artist Channel"],
        "thumbnails": []
      },
      "keywords": ["rick astley", "Never Gonna Give You Up"],
      "superTitle": "#RickAstley #NeverGonnaGiveYouUp #OfficialMusicVideo",
      "category": "Music",
      "isRatingsAllowed": true,
      "isLive": false,
      "isFamilySafe": true,
      "isPrivate": false,
      "isExclusive": false,
      "thumbnails": [
        { "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg", "width": 480, "height": 360 }
      ]
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get Short

Returns full metadata for a single short by its video `id`.

**Request**

```bash
GET /youtube/shorts/{id}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                                     |
| --------- | ------ | -------- | --------------------------------------------------------------- |
| `id`      | string | Yes      | 11-character video ID, 5–20 characters — passed in the URL path |

**Example**

```bash
curl -G "https://api.pullbay.com/youtube/shorts/kJQP7kiw5Fk" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "video",
      "id": "kJQP7kiw5Fk",
      "title": "I Gave My 1,000th Subscriber A Island",
      "status": "OK",
      "url": "https://www.youtube.com/watch?v=kJQP7kiw5Fk",
      "description": "Shorts edit of the full video",
      "duration": 58,
      "views": 42531468,
      "likes": 3198452,
      "comments": 18230,
      "channel": {
        "id": "UCX6OQ3DkcsbYNE6H8uQQuVA",
        "name": "MrBeast",
        "handle": "@MrBeast",
        "url": "https://www.youtube.com/channel/UCX6OQ3DkcsbYNE6H8uQQuVA",
        "subscriberCount": "421M subscribers",
        "badges": [],
        "thumbnails": []
      },
      "keywords": ["mrbeast", "shorts", "giveaway"],
      "superTitle": "#MrBeast #Shorts",
      "category": "Entertainment",
      "isRatingsAllowed": true,
      "isLive": false,
      "isFamilySafe": true,
      "isPrivate": false,
      "isExclusive": false,
      "thumbnails": [
        { "url": "https://i.ytimg.com/vi/kJQP7kiw5Fk/hqdefault.jpg", "width": 480, "height": 360 }
      ]
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get Comments

Returns top-level comments for a video. `sort` is `top` (default) or `latest`. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).

**Request**

```bash
GET /youtube/comments
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Description                                                                           |
| ---------- | ------- | -------- | ------------------------------------------------------------------------------------- |
| `id`       | string  | Yes      | 11-character video ID, 5–20 characters                                                |
| `sort`     | string  | No       | `top` (default) or `latest`                                                           |
| `cursor`   | string  | No       | Opaque pagination cursor from a previous response. Mutually exclusive with `maxItems` |
| `maxItems` | integer | No       | One-shot bulk pull, 1–500 results. Mutually exclusive with `cursor`                   |

**Example**

```bash
curl -G "https://api.pullbay.com/youtube/comments" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d id=dQw4w9WgXcQ \
  -d sort=top \
  -d maxItems=50
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "comment",
      "id": "UgwJsXlCbEu8-5nIK3h4AaABAg",
      "text": "Check out the latest Space Souvenirs Drop on the kurzgesagt shop",
      "likeCount": "1.8K",
      "replyCount": "59",
      "publishedTime": "1 month ago",
      "author": {
        "id": "UCsXVk37bltHxD1rDPwtNM8Q",
        "name": "@kurzgesagt",
        "thumbnails": "https://yt3.ggpht.com/ytc/AIdro_n1Ribd7LwdP_qKtqWL3ZDfIgv9M1d6g78VwpHGXVR2Ir4=s88-c-k-c0x00ffffff-no-rj"
      }
    }
  ],
  "pagination": { "page": null, "hasNextPage": true, "cursor": "Eg0SC2RRdzR3OVdnWGNRGAYyJSIRIgtkUXc0dzlXZ1hjURABGAA", "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of comments returned, not the number of requests.

***

### Search YouTube

Search YouTube. `mode` selects what to search for (`video`, `channel`, or `playlist`). When `mode=video`, results match the `YoutubeVideo` shape; other modes return shape variants. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).

**Request**

```bash
GET /youtube/search
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter    | Type    | Required | Description                                                                                                                       |
| ------------ | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `query`      | string  | Yes      | Search query, 1–100 characters                                                                                                    |
| `mode`       | string  | Yes      | `video`, `channel`, or `playlist`                                                                                                 |
| `sortBy`     | string  | No       | `relevance`, `rating`, `upload_date`, or `view_count`                                                                             |
| `lang`       | string  | No       | Language code, 2–5 characters                                                                                                     |
| `geo`        | string  | No       | Region code, 2–5 characters                                                                                                       |
| `duration`   | string  | No       | `short`, `medium`, or `long`                                                                                                      |
| `features`   | string  | No       | One feature filter — `360`, `hd`, `subtitles`, `creative_commons`, `3d`, `live`, `purchased`, `4k`, `location`, `hdr`, or `vr180` |
| `uploadDate` | string  | No       | `hour`, `today`, `week`, `month`, or `year`                                                                                       |
| `cursor`     | string  | No       | Opaque pagination cursor from a previous response. Mutually exclusive with `maxItems`                                             |
| `maxItems`   | integer | No       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`                                                               |

**Example**

```bash
curl -G "https://api.pullbay.com/youtube/search" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d query="javascript tutorial" \
  -d mode=video \
  -d sortBy=relevance \
  -d duration=long
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "video",
      "id": "EerdGm-ehJQ",
      "url": "https://www.youtube.com/watch?v=EerdGm-ehJQ",
      "title": "JavaScript Tutorial Full Course - Beginner to Pro",
      "description": "Get a certificate for this course",
      "thumbnails": [
        { "url": "https://i.ytimg.com/vi/EerdGm-ehJQ/hqdefault.jpg", "width": 480, "height": 360 }
      ],
      "richThumbnail": [],
      "channel": {
        "id": "UCW5YeuERMmlnqo4oq8vwUpg",
        "name": "freeCodeCamp.org",
        "handle": "@freecodecamp",
        "url": "https://www.youtube.com/channel/UCW5YeuERMmlnqo4oq8vwUpg",
        "subscriberCount": "10.4M subscribers",
        "badges": [],
        "thumbnails": []
      },
      "publishDate": "1 year ago",
      "views": "8531468",
      "duration": "22:15:57"
    }
  ],
  "pagination": { "page": null, "hasNextPage": true, "cursor": "CAUQAA", "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Using `maxItems` to pull more results in one call costs proportionally more than a single page.

***

### Get Hashtag

Returns videos tagged with `tag`. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).

**Request**

```bash
GET /youtube/hashtag
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Description                                                                           |
| ---------- | ------- | -------- | ------------------------------------------------------------------------------------- |
| `tag`      | string  | Yes      | Hashtag to look up, 1–100 characters, without the `#` (e.g. `shorts`)                 |
| `cursor`   | string  | No       | Opaque pagination cursor from a previous response. Mutually exclusive with `maxItems` |
| `maxItems` | integer | No       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`                   |

**Example**

```bash
curl -G "https://api.pullbay.com/youtube/hashtag" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d tag=shorts \
  -d maxItems=25
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "video",
      "id": "kJQP7kiw5Fk",
      "url": "https://www.youtube.com/watch?v=kJQP7kiw5Fk",
      "title": "I Gave My 1,000th Subscriber A Island #shorts",
      "description": "Shorts edit of the full video",
      "thumbnails": [
        { "url": "https://i.ytimg.com/vi/kJQP7kiw5Fk/hqdefault.jpg", "width": 480, "height": 360 }
      ],
      "richThumbnail": [],
      "channel": {
        "id": "UCX6OQ3DkcsbYNE6H8uQQuVA",
        "name": "MrBeast",
        "handle": "@MrBeast",
        "url": "https://www.youtube.com/channel/UCX6OQ3DkcsbYNE6H8uQQuVA",
        "subscriberCount": "421M subscribers",
        "badges": [],
        "thumbnails": []
      },
      "publishDate": "3 weeks ago",
      "views": "42531468",
      "duration": "0:58"
    }
  ],
  "pagination": { "page": null, "hasNextPage": true, "cursor": "CAUQAA", "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. Cost scales with the number of videos returned, not the number of requests.

***

### Get Trending

Returns currently trending videos. Use `geo` to target a country (e.g. `US`, `GB`).

**Request**

```bash
GET /youtube/trending
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Description                                   |
| --------- | ------ | -------- | --------------------------------------------- |
| `geo`     | string | No       | Region code, 2–5 characters (e.g. `US`, `GB`) |

**Example**

```bash
curl -G "https://api.pullbay.com/youtube/trending" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d geo=US
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "video",
      "id": "5qap5aO4i9A",
      "url": "https://www.youtube.com/watch?v=5qap5aO4i9A",
      "title": "lofi hip hop radio - beats to relax/study to",
      "description": "24/7 livestream",
      "thumbnails": [
        { "url": "https://i.ytimg.com/vi/5qap5aO4i9A/hqdefault.jpg", "width": 480, "height": 360 }
      ],
      "richThumbnail": [],
      "channel": {
        "id": "UCSJ4gkVC6NrvII8umztf0Ow",
        "name": "Lofi Girl",
        "handle": "@LofiGirl",
        "url": "https://www.youtube.com/channel/UCSJ4gkVC6NrvII8umztf0Ow",
        "subscriberCount": "13.7M subscribers",
        "badges": [],
        "thumbnails": []
      },
      "publishDate": "Streamed live",
      "views": "9823145",
      "duration": "LIVE"
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

## Object schemas

### Channel object

| Field                | Type    | Description                                                   |
| -------------------- | ------- | ------------------------------------------------------------- |
| `type`               | string  | Always `channel`                                              |
| `id`                 | string  | Channel ID                                                    |
| `name`               | string  | Channel display name                                          |
| `handle`             | string  | Channel handle (e.g. `@MrBeast`)                              |
| `url`                | string  | Channel URL                                                   |
| `description`        | string  | Channel "About" description                                   |
| `thumbnails`         | string  | Channel avatar image URL                                      |
| `isFamilySafe`       | boolean | Whether the channel is marked family-safe                     |
| `subscriberCount`    | string  | Subscriber count, formatted (e.g. `421M subscribers`)         |
| `availableCountries` | array   | Country codes where the channel is available                  |
| `tags`               | array   | Channel keyword tags                                          |
| `joinedAt`           | string  | Channel creation date, formatted (e.g. `Joined Feb 20, 2012`) |
| `viewCount`          | string  | All-time view count, formatted                                |
| `videoCount`         | integer | Total number of uploaded videos                               |

### Channel reference object

Lightweight channel reference embedded in video and comment results.

| Field             | Type                                    | Description                                     |
| ----------------- | --------------------------------------- | ----------------------------------------------- |
| `id`              | string                                  | Channel ID                                      |
| `name`            | string                                  | Channel display name                            |
| `handle`          | string                                  | Channel handle                                  |
| `url`             | string                                  | Channel URL                                     |
| `subscriberCount` | string                                  | Subscriber count, formatted                     |
| `badges`          | array                                   | Channel badges (e.g. `Official Artist Channel`) |
| `thumbnails`      | array of [Thumbnail](#thumbnail-object) | Channel avatar thumbnails at different sizes    |

### Video object

Video result returned by the channel videos/shorts/live-streams, search, hashtag, and trending endpoints.

| Field           | Type                                           | Description                                                            |
| --------------- | ---------------------------------------------- | ---------------------------------------------------------------------- |
| `type`          | string                                         | Always `video`                                                         |
| `id`            | string                                         | 11-character video ID                                                  |
| `url`           | string                                         | Video watch URL                                                        |
| `title`         | string                                         | Video title                                                            |
| `description`   | string                                         | Video description snippet                                              |
| `thumbnails`    | array of [Thumbnail](#thumbnail-object)        | Thumbnail images at different sizes                                    |
| `richThumbnail` | array of [Thumbnail](#thumbnail-object)        | Animated/rich preview thumbnails, if available                         |
| `channel`       | [Channel reference](#channel-reference-object) | Uploading channel                                                      |
| `publishDate`   | string                                         | Relative publish date (e.g. `1 year ago`)                              |
| `views`         | string                                         | View count as a string                                                 |
| `duration`      | string                                         | Video duration, formatted (e.g. `22:15:57`); `LIVE` for active streams |

### Video detail object

Full video metadata returned by the get video and get short endpoints.

| Field              | Type                                           | Description                                    |
| ------------------ | ---------------------------------------------- | ---------------------------------------------- |
| `type`             | string                                         | Always `video`                                 |
| `id`               | string                                         | 11-character video ID                          |
| `title`            | string                                         | Video title                                    |
| `status`           | string                                         | Lookup status (e.g. `OK`)                      |
| `url`              | string                                         | Video watch URL                                |
| `description`      | string                                         | Full video description                         |
| `duration`         | integer                                        | Length in seconds                              |
| `views`            | integer                                        | View count                                     |
| `likes`            | integer                                        | Like count                                     |
| `comments`         | integer                                        | Total comment count                            |
| `channel`          | [Channel reference](#channel-reference-object) | Uploading channel                              |
| `keywords`         | array                                          | Video tags/keywords                            |
| `superTitle`       | string                                         | Hashtag/super-title line shown above the title |
| `category`         | string                                         | YouTube content category (e.g. `Music`)        |
| `isRatingsAllowed` | boolean                                        | Whether likes/dislikes are enabled             |
| `isLive`           | boolean                                        | Whether the video is a live broadcast          |
| `isFamilySafe`     | boolean                                        | Whether the video is marked family-safe        |
| `isPrivate`        | boolean                                        | Whether the video is private                   |
| `isExclusive`      | boolean                                        | Whether the video is exclusive/members-only    |
| `thumbnails`       | array of [Thumbnail](#thumbnail-object)        | Thumbnail images at different sizes            |

### Comment object

| Field           | Type                                     | Description                                |
| --------------- | ---------------------------------------- | ------------------------------------------ |
| `type`          | string                                   | Always `comment`                           |
| `id`            | string                                   | Comment ID                                 |
| `text`          | string                                   | Comment body text                          |
| `likeCount`     | string                                   | Like count, formatted (e.g. `1.8K`)        |
| `replyCount`    | string                                   | Reply count, formatted                     |
| `publishedTime` | string                                   | Relative publish time (e.g. `1 month ago`) |
| `author`        | [Comment author](#comment-author-object) | Commenter                                  |

### Comment author object

| Field        | Type   | Description                  |
| ------------ | ------ | ---------------------------- |
| `id`         | string | Author's channel ID          |
| `name`       | string | Author's display name/handle |
| `thumbnails` | string | Author's avatar image URL    |

### Thumbnail object

| Field    | Type    | Description            |
| -------- | ------- | ---------------------- |
| `url`    | string  | Thumbnail image URL    |
| `width`  | integer | Image width in pixels  |
| `height` | integer | Image height in pixels |

***

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                                                                                          |
| ------ | ---------------------------------------------------------------------------------------------------------------- |
| `400`  | Bad request — missing/invalid parameters, or more than one of `id`/`handle`/`channelUrl` supplied to Get Channel |
| `402`  | Insufficient credits                                                                                             |
| `404`  | Channel, video, short, or comment thread not found                                                               |

## Channel, Video, and Short IDs

* **Channel ID**: the `UC...` string in a channel URL, e.g. `https://www.youtube.com/channel/UCX6OQ3DkcsbYNE6H8uQQuVA` → `UCX6OQ3DkcsbYNE6H8uQQuVA`. Pass it as the `id` path/query parameter depending on the endpoint.
* **Handle**: the `@name` shown on a channel's page, e.g. `https://www.youtube.com/@MrBeast` → `@MrBeast`.
* **Video/Short ID**: the 11-character `v=` value in a watch URL or the trailing path segment of a shorts URL, e.g. `https://www.youtube.com/watch?v=dQw4w9WgXcQ` → `dQw4w9WgXcQ`. The same ID format is used for both regular videos and shorts — use `/youtube/video` or `/youtube/shorts/{id}` depending on which you're looking up.

## Pagination Notes

* `GET /youtube/channel`, `GET /youtube/video`, `GET /youtube/shorts/{id}`, and `GET /youtube/trending` are single-object or fixed-size lookups and have no pagination parameters.
* All list endpoints (channel videos, channel shorts, channel live streams, comments, search, hashtag) use cursor-based pagination: pass the `cursor` value from `pagination.cursor` in a response to fetch the next page, or use `maxItems` instead to pull a bulk batch in one call. The two are mutually exclusive.


# App Store

The App Store service provides access to iOS application data including reviews, ratings, and app metadata.

## Endpoints

| Endpoint                      | Description                                     | Pagination |
| ----------------------------- | ----------------------------------------------- | ---------- |
| `GET /api/appstore/reviews`   | Get reviews for an application                  | Standard   |
| `GET /api/appstore/apps`      | Get a single application by App ID or Bundle ID | —          |
| `GET /api/appstore/search`    | Search applications by term                     | Standard   |
| `GET /api/appstore/similar`   | Get similar applications                        | —          |
| `GET /api/appstore/developer` | Get all applications by a developer             | —          |

## Get Reviews

Fetch reviews for an iOS app with standard pagination.

### Request

```http
GET /api/appstore/reviews
Authorization: Bearer YOUR_API_KEY
```

### Parameters

| Parameter | Type    | Required | Description                                                        |
| --------- | ------- | -------- | ------------------------------------------------------------------ |
| `app_id`  | string  | Yes      | App Store app ID (numeric). May be supplied as `bundleId` instead. |
| `country` | string  | No       | Country code (default: `us`)                                       |
| `sort`    | string  | No       | Sort order: `recent`, `helpful`, `critical`                        |
| `limit`   | integer | No       | Results per page (default: 25, max: 100)                           |
| `cursor`  | string  | No       | Pagination cursor                                                  |

### Example

```bash
curl "https://api.pullbay.com/v1/app-store/reviews?app_id=284882215&country=us&limit=25" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Credit cost

1 credit per page.

### Response

```json
{
  "data": [
    {
      "id": "10293847561",
      "author": "HappyUser",
      "rating": 5,
      "title": "Best app ever",
      "content": "I've been using this app for years and it keeps getting better...",
      "date": "2024-01-15T10:30:00Z",
      "version": "5.2.1",
      "helpful_count": 42
    }
  ],
  "pagination": {
    "next_cursor": "eyJwYWdlIjoyLCJsaW1pdCI6MjV9",
    "has_more": true
  },
  "meta": {
    "credits_used": 1,
    "request_id": "req_abc123"
  }
}
```

## Get App

Returns one application. Provide exactly one of `appId` or `bundleId`.

### Request

```http
GET /api/appstore/apps
Authorization: Bearer YOUR_API_KEY
```

### Parameters

| Parameter  | Type   | Required | Description                                                             |
| ---------- | ------ | -------- | ----------------------------------------------------------------------- |
| `appId`    | string | No\*     | App Store app ID (numeric). Pattern: `^\d{8,12}$`. Example: `389801252` |
| `bundleId` | string | No\*     | App bundle ID (max 255). Pattern: `^[A-Za-z0-9.]+$`                     |
| `country`  | string | No       | Country code (2 letters, ISO 3166-1 alpha-2)                            |
| `lang`     | string | No       | Language code. Pattern: `^[a-z]{2,3}(-[a-z]{2,3})?$`                    |

\* Provide exactly one of `appId` or `bundleId`.

### Example

```bash
curl "https://api.pullbay.com/appstore/apps?appId=389801252" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Credit cost

1 credit per request + 1 credit per item.

### Response

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": 389801252,
      "appId": "com.burbn.instagram",
      "title": "Instagram",
      "url": "https://apps.apple.com/us/app/instagram/id389801252?uo=4",
      "description": "Little moments lead to big friendships. Share yours on Instagram.",
      "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/.../512x512bb.jpg",
      "genres": ["Photo & Video", "Social Networking"],
      "genreIds": ["6008", "6005"],
      "primaryGenre": "Photo & Video",
      "primaryGenreId": 6008,
      "contentRating": "12+",
      "languages": ["EN", "FR", "DE", "JA", "ES"],
      "size": "563586048",
      "requiredOsVersion": "16.3",
      "released": "2010-10-06T08:12:41Z",
      "updated": "2026-04-20T17:48:35Z",
      "releaseNotes": "Performance optimizations and stability improvements for a smoother, more reliable experience.",
      "version": "426.0.0",
      "price": 0,
      "currency": "USD",
      "free": true,
      "developerId": 389801255,
      "developer": "Instagram, Inc.",
      "developerUrl": "https://apps.apple.com/us/developer/instagram-inc/id389801255?uo=4",
      "developerWebsite": "http://instagram.com/",
      "score": 4.69066,
      "reviews": 28962365,
      "currentVersionScore": 4.69066,
      "currentVersionReviews": 28962365,
      "screenshots": ["https://is1-ssl.mzstatic.com/image/thumb/.../392x696bb.jpg"],
      "ipadScreenshots": [],
      "appletvScreenshots": [],
      "supportedDevices": ["iPhone5s-iPhone5s", "iPadAir-iPadAir", "iPhone16Pro-iPhone16Pro"]
    }
  ],
  "pricing": {
    "creditsCharged": 2
  }
}
```

## Search Apps

Search the App Store for applications matching `term`. Use `page` for pagination or `maxItems` for a one-shot bulk pull (mutually exclusive).

### Request

```http
GET /api/appstore/search
Authorization: Bearer YOUR_API_KEY
```

### Parameters

| Parameter  | Type    | Required | Description                                                                  |
| ---------- | ------- | -------- | ---------------------------------------------------------------------------- |
| `term`     | string  | Yes      | Search term (min 1, max 255). Example: `ai dungeon`                          |
| `country`  | string  | No       | Country code (2 letters, ISO 3166-1 alpha-2)                                 |
| `lang`     | string  | No       | Language code. Pattern: `^[a-z]{2,3}(-[a-z]{2,3})?$`                         |
| `maxItems` | integer | No       | One-shot bulk pull (min 1, max 200). Mutually exclusive with `page`.         |
| `page`     | integer | No       | Page number (min 1, max 5). Mutually exclusive with `maxItems`. Example: `1` |

### Example

```bash
curl "https://api.pullbay.com/appstore/search?term=ai%20dungeon&page=1" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Credit cost

1 credit per request + 1 credit per item.

### Response

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": 389801252,
      "appId": "com.burbn.instagram",
      "title": "Instagram",
      "url": "https://apps.apple.com/us/app/instagram/id389801252?uo=4",
      "description": "Little moments lead to big friendships. Share yours on Instagram.",
      "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/.../512x512bb.jpg",
      "genres": ["Photo & Video", "Social Networking"],
      "genreIds": ["6008", "6005"],
      "primaryGenre": "Photo & Video",
      "primaryGenreId": 6008,
      "contentRating": "12+",
      "languages": ["EN", "FR", "DE", "JA", "ES"],
      "size": "563586048",
      "requiredOsVersion": "16.3",
      "released": "2010-10-06T08:12:41Z",
      "updated": "2026-04-20T17:48:35Z",
      "releaseNotes": "Performance optimizations and stability improvements for a smoother, more reliable experience.",
      "version": "426.0.0",
      "price": 0,
      "currency": "USD",
      "free": true,
      "developerId": 389801255,
      "developer": "Instagram, Inc.",
      "developerUrl": "https://apps.apple.com/us/developer/instagram-inc/id389801255?uo=4",
      "developerWebsite": "http://instagram.com/",
      "score": 4.69066,
      "reviews": 28962365,
      "currentVersionScore": 4.69066,
      "currentVersionReviews": 28962365,
      "screenshots": ["https://is1-ssl.mzstatic.com/image/thumb/.../392x696bb.jpg"],
      "ipadScreenshots": [],
      "appletvScreenshots": [],
      "supportedDevices": ["iPhone5s-iPhone5s", "iPadAir-iPadAir", "iPhone16Pro-iPhone16Pro"]
    }
  ],
  "pagination": {
    "page": 1,
    "hasNextPage": null,
    "cursor": null,
    "offset": null
  },
  "pricing": {
    "creditsCharged": 2
  }
}
```

## Get Similar Apps

Returns applications similar to the supplied one. Provide exactly one of `appId` or `bundleId`.

### Request

```http
GET /api/appstore/similar
Authorization: Bearer YOUR_API_KEY
```

### Parameters

| Parameter  | Type   | Required | Description                                                             |
| ---------- | ------ | -------- | ----------------------------------------------------------------------- |
| `appId`    | string | No\*     | App Store app ID (numeric). Pattern: `^\d{8,12}$`. Example: `389801252` |
| `bundleId` | string | No\*     | App bundle ID (max 255). Pattern: `^[A-Za-z0-9.]+$`                     |
| `country`  | string | No       | Country code (2 letters, ISO 3166-1 alpha-2)                            |
| `lang`     | string | No       | Language code. Pattern: `^[a-z]{2,3}(-[a-z]{2,3})?$`                    |

\* Provide exactly one of `appId` or `bundleId`.

### Example

```bash
curl "https://api.pullbay.com/appstore/similar?appId=389801252" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Credit cost

1 credit per request + 1 credit per item.

### Response

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": 389801252,
      "appId": "com.burbn.instagram",
      "title": "Instagram",
      "url": "https://apps.apple.com/us/app/instagram/id389801252?uo=4",
      "description": "Little moments lead to big friendships. Share yours on Instagram.",
      "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/.../512x512bb.jpg",
      "genres": ["Photo & Video", "Social Networking"],
      "genreIds": ["6008", "6005"],
      "primaryGenre": "Photo & Video",
      "primaryGenreId": 6008,
      "contentRating": "12+",
      "languages": ["EN", "FR", "DE", "JA", "ES"],
      "size": "563586048",
      "requiredOsVersion": "16.3",
      "released": "2010-10-06T08:12:41Z",
      "updated": "2026-04-20T17:48:35Z",
      "releaseNotes": "Performance optimizations and stability improvements for a smoother, more reliable experience.",
      "version": "426.0.0",
      "price": 0,
      "currency": "USD",
      "free": true,
      "developerId": 389801255,
      "developer": "Instagram, Inc.",
      "developerUrl": "https://apps.apple.com/us/developer/instagram-inc/id389801255?uo=4",
      "developerWebsite": "http://instagram.com/",
      "score": 4.69066,
      "reviews": 28962365,
      "currentVersionScore": 4.69066,
      "currentVersionReviews": 28962365,
      "screenshots": ["https://is1-ssl.mzstatic.com/image/thumb/.../392x696bb.jpg"],
      "ipadScreenshots": [],
      "appletvScreenshots": [],
      "supportedDevices": ["iPhone5s-iPhone5s", "iPadAir-iPadAir", "iPhone16Pro-iPhone16Pro"]
    }
  ],
  "pricing": {
    "creditsCharged": 2
  }
}
```

## Get Developer Apps

Returns the list of applications published by a single developer.

### Request

```http
GET /api/appstore/developer
Authorization: Bearer YOUR_API_KEY
```

### Parameters

| Parameter     | Type   | Required | Description                                                         |
| ------------- | ------ | -------- | ------------------------------------------------------------------- |
| `developerId` | string | Yes      | Developer ID (numeric). Pattern: `^\d{8,12}$`. Example: `389801255` |
| `country`     | string | No       | Country code (2 letters, ISO 3166-1 alpha-2)                        |

### Example

```bash
curl "https://api.pullbay.com/appstore/developer?developerId=389801255" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Credit cost

1 credit per request + 1 credit per item.

### Response

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": 389801252,
      "appId": "com.burbn.instagram",
      "title": "Instagram",
      "url": "https://apps.apple.com/us/app/instagram/id389801252?uo=4",
      "description": "Little moments lead to big friendships. Share yours on Instagram.",
      "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/.../512x512bb.jpg",
      "genres": ["Photo & Video", "Social Networking"],
      "genreIds": ["6008", "6005"],
      "primaryGenre": "Photo & Video",
      "primaryGenreId": 6008,
      "contentRating": "12+",
      "languages": ["EN", "FR", "DE", "JA", "ES"],
      "size": "563586048",
      "requiredOsVersion": "16.3",
      "released": "2010-10-06T08:12:41Z",
      "updated": "2026-04-20T17:48:35Z",
      "releaseNotes": "Performance optimizations and stability improvements for a smoother, more reliable experience.",
      "version": "426.0.0",
      "price": 0,
      "currency": "USD",
      "free": true,
      "developerId": 389801255,
      "developer": "Instagram, Inc.",
      "developerUrl": "https://apps.apple.com/us/developer/instagram-inc/id389801255?uo=4",
      "developerWebsite": "http://instagram.com/",
      "score": 4.69066,
      "reviews": 28962365,
      "currentVersionScore": 4.69066,
      "currentVersionReviews": 28962365,
      "screenshots": ["https://is1-ssl.mzstatic.com/image/thumb/.../392x696bb.jpg"],
      "ipadScreenshots": [],
      "appletvScreenshots": [],
      "supportedDevices": ["iPhone5s-iPhone5s", "iPadAir-iPadAir", "iPhone16Pro-iPhone16Pro"]
    }
  ],
  "pricing": {
    "creditsCharged": 2
  }
}
```

## Finding app IDs

The `app_id` is the numeric ID in the App Store URL:

```
https://apps.apple.com/us/app/facebook/id284882215
                                          └────────┘
                                           app_id
```

## Country codes

Use ISO 3166-1 alpha-2 codes: `us`, `gb`, `de`, `fr`, `jp`, etc.

## Review object

| Field      | Type    | Description                   |
| ---------- | ------- | ----------------------------- |
| `id`       | string  | Unique review identifier      |
| `date`     | string  | ISO 8601 timestamp            |
| `userName` | string  | Reviewer username             |
| `userUrl`  | string  | URL to the reviewer's profile |
| `version`  | string  | App version reviewed          |
| `score`    | integer | Star rating (1–5)             |
| `title`    | string  | Review title                  |
| `text`     | string  | Review body text              |
| `url`      | string  | URL to the review             |
| `country`  | string  | Country code of the review    |

## Application object

| Field                   | Type      | Description                          |
| ----------------------- | --------- | ------------------------------------ |
| `id`                    | integer   | Numeric App Store ID                 |
| `appId`                 | string    | Bundle ID                            |
| `title`                 | string    | App name                             |
| `url`                   | string    | App Store URL                        |
| `description`           | string    | App description                      |
| `icon`                  | string    | Icon image URL                       |
| `genres`                | string\[] | Genre names                          |
| `genreIds`              | string\[] | Genre IDs                            |
| `primaryGenre`          | string    | Primary genre name                   |
| `primaryGenreId`        | integer   | Primary genre ID                     |
| `contentRating`         | string    | Age rating (e.g. `12+`)              |
| `languages`             | string\[] | Supported languages                  |
| `size`                  | string    | App size in bytes                    |
| `requiredOsVersion`     | string    | Minimum required OS version          |
| `released`              | string    | Initial release date (ISO 8601)      |
| `updated`               | string    | Last update date (ISO 8601)          |
| `releaseNotes`          | string    | Notes for the current version        |
| `version`               | string    | Current version                      |
| `price`                 | number    | Price                                |
| `currency`              | string    | Currency code                        |
| `free`                  | boolean   | Whether the app is free              |
| `developerId`           | integer   | Developer ID                         |
| `developer`             | string    | Developer name                       |
| `developerUrl`          | string    | Developer App Store URL              |
| `developerWebsite`      | string    | Developer website                    |
| `score`                 | number    | Overall rating                       |
| `reviews`               | integer   | Total review count                   |
| `currentVersionScore`   | number    | Rating for the current version       |
| `currentVersionReviews` | integer   | Review count for the current version |
| `screenshots`           | string\[] | iPhone screenshot URLs               |
| `ipadScreenshots`       | string\[] | iPad screenshot URLs                 |
| `appletvScreenshots`    | string\[] | Apple TV screenshot URLs             |
| `supportedDevices`      | string\[] | Supported device identifiers         |


# Help Center

Get help with Pullbay

<h2 align="center">How can we help?</h2>

<p align="center">Browse topics below or contact our support team.</p>

<p align="center"><a href="mailto:support@pullbay.com" class="button primary">Contact support</a> <a href="https://docs.pullbay.com/documentation" class="button secondary">View documentation</a></p>

<table data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>🚀</strong></td><td><strong>Getting started</strong></td><td>Create an account and make your first call</td><td><a href="https://docs.pullbay.com/documentation/getting-started/quickstart">https://docs.pullbay.com/documentation/getting-started/quickstart</a></td></tr><tr><td><strong>🔑</strong></td><td><strong>Authentication</strong></td><td>API keys and security</td><td><a href="https://docs.pullbay.com/documentation/getting-started/authentication">https://docs.pullbay.com/documentation/getting-started/authentication</a></td></tr><tr><td><strong>💳</strong></td><td><strong>Billing</strong></td><td>Credits, plans, and invoices</td><td><a href="https://docs.pullbay.com/documentation/plans-and-usage/billing">https://docs.pullbay.com/documentation/plans-and-usage/billing</a></td></tr><tr><td><strong>⚡</strong></td><td><strong>Rate limits</strong></td><td>Understanding and handling limits</td><td><a href="https://docs.pullbay.com/documentation/plans-and-usage/rate-limits">https://docs.pullbay.com/documentation/plans-and-usage/rate-limits</a></td></tr><tr><td><strong>🔧</strong></td><td><strong>Errors</strong></td><td>Troubleshooting API errors</td><td><a href="https://docs.pullbay.com/documentation/concepts/errors-and-retries">https://docs.pullbay.com/documentation/concepts/errors-and-retries</a></td></tr></tbody></table>

***

## Frequently asked questions

### Account & billing

<details>

<summary><strong>How do credits work?</strong></summary>

Credits are consumed when you make successful API requests. The cost depends on the endpoint and amount of data returned. Failed requests don't consume credits.

[Learn more about credits →](https://docs.pullbay.com/documentation/plans-and-usage/credits)

</details>

<details>

<summary><strong>What happens when I run out of credits?</strong></summary>

When your balance reaches zero, API requests will be rejected with a `402 Payment Required` error. You can add credits anytime from your dashboard without changing your plan.

</details>

<details>

<summary><strong>Can I get a refund?</strong></summary>

Credits are non-refundable but never expire. If you believe you were charged incorrectly, contact support with your request details.

</details>

### API & integration

<details>

<summary><strong>What's the difference between standard and managed pagination?</strong></summary>

**Standard pagination** — You control the flow, requesting one page at a time. Best for custom apps where you need partial data.

**Managed pagination** — Pullbay fetches all pages and returns aggregated results. Best for n8n/Make.com workflows.

Both cost the same per amount of data.

[Learn more about pagination→](https://docs.pullbay.com/documentation/concepts/pagination)

</details>

<details>

<summary><strong>Why am I getting rate limited?</strong></summary>

Rate limits depend on your plan. Check the `remaining` header in responses. If you consistently hit limits, consider upgrading your plan or optimizing your request patterns.

[Learn more about rate limits →](https://docs.pullbay.com/documentation/plans-and-usage/rate-limits)

</details>

<details>

<summary><strong>Can I use Pullbay with n8n or Make.com?</strong></summary>

Yes! Pullbay is designed for automation tools. Use managed pagination endpoints (`/all`) to get complete datasets in one request without writing pagination loops.

[See n8n integration guide →](https://docs.pullbay.com/documentation/integrations/pullbay-x-n8n-integration)

</details>

### Data & reliability

<details>

<summary><strong>How fresh is the data?</strong></summary>

Data is fetched in real-time when you make a request. We don't cache results, so you always get the latest available data from the source.

</details>

<details>

<summary><strong>What happens if an endpoint is down?</strong></summary>

Check the service health in your dashboard. If an endpoint is experiencing issues, requests may fail or return partial results. We handle retries internally when possible.

[Check service health →](https://docs.pullbay.com/documentation/platform-best-practices/service-health)

</details>

<details>

<summary><strong>Can I request a new data source?</strong></summary>

Yes! Contact us through the dashboard or email with:

* The data source you need
* Your use case
* Expected volume

We prioritize based on demand and feasibility.

</details>

***

## Contact support

### Before contacting us

1. Check the [documentation](https://docs.pullbay.com/documentation)
2. Review the FAQs above
3. Check [service health](https://docs.pullbay.com/documentation/platform-best-practices/service-health) for known issues

### How to reach us

**Email:** <support@pullbay.com>

**Include in your request:**

* Your account email
* Request ID (from API response)
* What you expected vs. what happened
* Any error messages

### Response times

| Plan    | Standard issues | Critical issues |
| ------- | --------------- | --------------- |
| Free    | Best effort     | Best effort     |
| Hobby   | 48 hours        | 24 hours        |
| Builder | 24 hours        | 4 hours         |
| Scale   | 4 hours         | 1 hour          |


# Introduction

Pullbay is a data API platform. You send a request, you get structured data back. That's it.

Behind the scenes, we maintain a library of data extraction endpoints for popular platforms and services. See [Services Overview](/documentation/api-and-references/services-overview) for the current service catalog. Each endpoint is monitored, tested, and updated automatically — so you don't have to worry about scraper maintenance.

## The problem we solve

Scraping public data sounds simple until you try to do it reliably:

* Websites change their structure without warning
* Anti-bot systems block your requests
* Proxy management becomes a full-time job
* Rate limits and retries need careful handling
* Data formats vary and need constant normalization

Most teams either spend months building and maintaining scrapers, or give up and buy expensive enterprise solutions.

Pullbay sits in the middle. We handle the hard parts so you can focus on using the data.

## How it works

```
1. You send a request    →    GET /api/appstore/reviews?app_id=123
2. Pullbay processes it  →    Fetches data, handles retries, normalizes output
3. You get JSON back     →    { "data": [...], "pagination": {...} }
4. Credits are deducted  →    Based on data returned
```

When you make a request, Pullbay:

1. **Validates your request** — Checks [authentication](/documentation/getting-started/authentication), parameters, and [rate limits](/documentation/plans-and-usage/rate-limits)
2. **Routes to the right extractor** — Each endpoint has specialized extraction logic
3. **Handles failures automatically** — Retries transient errors and recovers from common failures. See [Errors and Retries](/documentation/concepts/errors-and-retries)
4. **Normalizes the data** — Converts raw responses into consistent JSON schemas
5. **Returns results** — Delivers data with [pagination ](/documentation/concepts/pagination)metadata and usage info

You don't see any of this complexity. You just get clean data.

## What you get

| Feature               | What it means for you                        |
| --------------------- | -------------------------------------------- |
| Pre-built endpoints   | No scraper development required              |
| Automatic maintenance | We fix breakages before you notice them      |
| Structured output     | Consistent JSON schemas across all endpoints |
| Credit-based pricing  | Pay for what you use, scale as needed        |
| Two pagination modes  | Manual control or automatic aggregation      |

## Who Pullbay is for

**Developers and engineers** — You need data from external platforms for your application. Skip the scraper development, make HTTP requests, get JSON back.

**Data and analytics teams** — You're building reports or datasets. Stable endpoints that just work, no Python scripts to debug or proxies to rotate.

**Automation builders** — You're using n8n or Make.com. Managed pagination endpoints give you all the data in one request, no loops required.

**Product teams** — You're building features that depend on external data. Reliable APIs with transparent health metrics.

## What Pullbay is not

Pullbay is not a general-purpose web scraper. You can't point it at any URL and expect results.

We provide curated, maintained endpoints for specific data sources. If a source isn't in our library, you can request it — but we focus on quality and reliability over coverage.

## Related topics

Use these pages as your next step:

* [Quickstart](/documentation/getting-started/quickstart) — Set up your account and make your first request
* [Authentication](/documentation/getting-started/authentication) — Learn how API keys work
* [First API Call ](/documentation/getting-started/first-api-call)— Walk through a real request and response
* [Services Overview](/documentation/api-and-references/services-overview) — See available services and endpoint types
* [Pagination](/documentation/concepts/pagination) — Understand standard and managed pagination
* [Rate Limits](/documentation/plans-and-usage/rate-limits) — Plan request volume and backoff behavior


# Glossary

A comprehensive reference of terminology used throughout Pullbay documentation and the API.

***

## A

### API Key

A unique authentication credential used to authorize API requests. Pullbay issues API keys with prefixes indicating their environment: `test_` for testing/development and `live_` for production use. API keys should be kept secret and never shared publicly. See [Authentication](/documentation/getting-started/authentication) for usage examples.

### Auto-reload

An automatic credit replenishment feature that purchases additional credits when your balance falls below a specified threshold. Useful for preventing service interruptions due to unexpected usage spikes. Configure auto-reload in your dashboard under **Billing → Auto-reload Settings**.

***

## B

### Base URL

The root endpoint for all Pullbay API requests: `https://api.pullbay.com`. All API calls are made relative to this base URL. For example: `GET https://api.pullbay.com/appstore/reviews`.

### Bearer Token

The authentication method used by Pullbay. Include your API key in the HTTP Authorization header as a Bearer token:

```bash
Authorization: Bearer YOUR_API_KEY
```

See [Authentication](/documentation/getting-started/authentication) for details.

***

## C

### Circuit Breaker

A fault-tolerance pattern used by Pullbay to handle cascading failures. When a data source becomes unreliable, the circuit breaker temporarily stops requests to that source, preventing repeated failures. The circuit automatically closes (resumes) once the source recovers. Learn more in [Error Handling](/documentation/support/troubleshooting).

### Credit

The billing unit for Pullbay API requests. Each successful API call consumes a certain number of credits based on the data returned. Credits are prepaid and deducted from your account balance. See [Credits ](/documentation/plans-and-usage/credits)for detailed information on consumption, costs, and management.

### Credit Balance

The number of credits currently available in your Pullbay account. This is displayed in your dashboard and in the `X-Credits-Remaining` response header. When balance reaches zero, API requests are rejected until you add more credits.

### Cursor

An opaque string used for pagination in Pullbay API responses. Cursors represent a position in a dataset and are used with the `cursor` parameter to fetch the next page of results. Cursors should be treated as strings and not parsed or modified. See [Pagination](/documentation/concepts/pagination) for usage.

***

## D

### Degraded Status

A temporary reduction in service quality, typically due to upstream platform changes or maintenance. When Pullbay experiences degraded status, endpoints may return partial results or experience higher-than-normal latency.

***

## E

### Endpoint

A specific API path that provides access to data from a particular source or service. For example, `/appstore/reviews` is an endpoint that returns reviews for an iOS app. Each endpoint has its own parameters, response format, and credit cost. See [Endpoints](https://docs.pullbay.com/api-reference/) for a complete list.

### Exponential Backoff

A retry strategy that increases the wait time between failed request attempts exponentially (1s, 2s, 4s, 8s, etc.). Pullbay SDKs implement exponential backoff automatically, but manual implementations should follow this pattern to respect rate limits and avoid overwhelming the API. Learn more in [Rate Limiting.](/documentation/plans-and-usage/rate-limits)

***

## J

### JSON Schema

A standardized format for describing the structure of JSON data. Pullbay API responses conform to published JSON schemas, ensuring consistent structure across all responses. Schemas define required fields, data types, and validation rules. See [API Reference](/documentation/api-and-references/services-overview) for endpoint-specific schemas.

***

## M

### Managed Pagination

An automatic pagination mode provided by Pullbay where the API automatically aggregates all available data and returns it in a single response. This mode simplifies data collection but consumes more credits based on total data volume. Compare to Standard Pagination. See [Pagination](/documentation/concepts/pagination).

***

## P

### Partial Success

A response state where an API request succeeds in returning some data but encounters errors for other portions. For example, if you request reviews for multiple apps and one source fails, the response includes successful data along with error information for the failed source. Credits are charged only for data successfully returned.

### Plan

A subscription tier that determines your monthly credit allocation, rate limit, feature access, and support level. Pullbay offers four plans: Free, Starter, Growth, and Scale. See [Plans](/documentation/plans-and-usage/plans) for details on each tier.

***

## R

### Rate Limit

A restriction on the maximum number of API requests you can make per minute, determined by your subscription plan. Free plans allow 10 requests/minute, while Scale plans allow 1,000+ requests/minute. Exceeding your rate limit returns a `429 Too Many Requests` response. See [Rate Limiting.](/documentation/plans-and-usage/rate-limits)

### Request ID

A unique identifier (`meta.request_id`) assigned to each API request for tracking and debugging purposes. Request IDs are included in every API response and are useful for referencing specific requests in support tickets or logs:

```json
{
  "meta": {
    "request_id": "req_1681234567890",
    "status": "success"
  }
}
```

***

## S

### Service

A category of related data sources organized within Pullbay. Currently, the **Apple App Store** service provides access to app reviews, ratings, metadata, and pricing. Additional services (Google Play Store, etc.) are planned. Each service has multiple endpoints, each with its own data structure and credit cost.

### Standard Pagination

A pagination mode where the API returns a limited number of items per request and provides pagination controls (limit, offset, cursor) to fetch additional pages. Allows granular control over data retrieval but requires multiple requests. Compare to Managed Pagination. See [Pagination](/documentation/concepts/pagination).

***

## Additional Resources

* [API Reference](https://docs.pullbay.com/api-reference/) — Complete endpoint documentation
* [Authentication ](/documentation/getting-started/authentication)— API key management and Bearer token usage
* [Error Handling](/documentation/support/troubleshooting) — Understanding error responses and troubleshooting
* [Rate Limits](/documentation/plans-and-usage/rate-limits) — Managing request rates and backoff strategies
* [Pagination ](/documentation/concepts/pagination)— Fetching data across multiple pages
* [Status Page](/documentation/platform-best-practices/service-health) — Real-time service status and incidents


# Quickstart

Get up and running with the Pullbay API in minutes. This guide covers account setup, API key generation, and your first request — using the patterns that apply to every endpoint across every service.

***

## What Pullbay Provides

Pullbay is a data API platform. One API key, one request format, and one response envelope give you access to data from:

| Service           | Endpoints                                                            |
| ----------------- | -------------------------------------------------------------------- |
| **App Store**     | reviews, app details, search, similar apps, developer apps           |
| **Google Play**   | reviews, app details, search, similar apps, permissions, data safety |
| **YouTube**       | search, channel, comments, shorts, hashtags                          |
| **Twitter / X**   | user, handle, tweet, search                                          |
| **Instagram**     | user, posts, reels, stories, tagged posts, post comments             |
| **TikTok**        | user, post, post comments, hashtag, search                           |
| **Reddit**        | subreddit, posts, post comments, user, user posts, search            |
| **Google Search** | search                                                               |
| **Google News**   | search, category                                                     |
| **Google Maps**   | place search, place details, reviews                                 |
| **DuckDuckGo**    | search, images, news, videos                                         |
| **Yelp**          | place search, place details, reviews                                 |
| **Apartments**    | property search, property details, images, reviews                   |

All endpoints share the same authentication method, request structure, response envelope, and error format. Learn it once, use it everywhere.

***

{% stepper %}
{% step %}

## Create Your Account

1. Visit [dashboard.pullbay.com](https://dashboard.pullbay.com/)
2. Click **Sign Up**, enter your email, and verify your address (or sign in with Google)
3. Your account is active with free credits — no credit card required
   {% endstep %}

{% step %}

## Get Your API Key

1. Sign in to your [dashboard](https://dashboard.pullbay.com/)
2. Navigate to **API Keys** in the left sidebar
3. Click **Create New Key**, give it a name (e.g. "Development"), and click **Create**
4. Copy the key — it is shown only once

Store the key in an environment variable. Never commit it to version control.

```bash
# .env
PULLBAY_API_KEY=your_key_here
```

{% endstep %}

{% step %}

## Make Your First Request

Every Pullbay endpoint follows the same URL pattern:

```
GET https://api.pullbay.com/{service}/{endpoint}
```

Authentication is always the same Bearer header:

```
Authorization: Bearer YOUR_API_KEY
```

Here's a concrete first request — fetching App Store reviews. The same pattern works for every other service and endpoint.

{% tabs %}
{% tab title="cURL" %}

```bash
curl "https://api.pullbay.com/appstore/reviews?appId=284882215&maxItems=5" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

{% endtab %}

{% tab title="Python" %}

```python
import requests
import os

response = requests.get(
    "https://api.pullbay.com/appstore/reviews",
    params={"appId": "284882215", "maxItems": 5},
    headers={"Authorization": f"Bearer {os.environ['PULLBAY_API_KEY']}"},
    timeout=30,
)
print(response.json())
```

{% endtab %}

{% tab title="Node.js" %}

```javascript
const response = await fetch(
  'https://api.pullbay.com/appstore/reviews?appId=284882215&maxItems=5',
  { headers: { 'Authorization': `Bearer ${process.env.PULLBAY_API_KEY}` } }
);
const data = await response.json();
console.log(data);
```

{% endtab %}
{% endtabs %}

The same structure — `GET`, `Authorization` header, query parameters — applies to every other endpoint:

```bash
# YouTube channel search
curl "https://api.pullbay.com/youtube/search?query=openai&maxItems=5" \
  -H "Authorization: Bearer YOUR_API_KEY"

# Google Play reviews
curl "https://api.pullbay.com/google-play/reviews?appId=com.instagram.android&maxItems=5" \
  -H "Authorization: Bearer YOUR_API_KEY"

# Google Search
curl "https://api.pullbay.com/google-search/search?query=best+coffee+shops&maxItems=5" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

{% endstep %}

{% step %}

## Understand the Response

Every Pullbay endpoint returns the same envelope regardless of service:

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "13997548250",
      "score": 5,
      "title": "Great app!",
      "text": "This app has changed how I work...",
      "userName": "HappyUser123",
      "date": "2026-01-15T10:30:00-07:00",
      "version": "5.2.1",
      "country": "US",
      "url": "https://itunes.apple.com/us/review?id=284882215"
    }
  ],
  "pagination": {
    "page": 1,
    "hasNextPage": true,
    "cursor": null,
    "offset": null
  },
  "pricing": {
    "creditsCharged": 5
  }
}
```

| Field                    | Description                                                                   |
| ------------------------ | ----------------------------------------------------------------------------- |
| `requestId`              | Unique request ID — log this for every call; required when contacting support |
| `success`                | `true` on success, `false` on error                                           |
| `data`                   | Array of result objects — field names vary by endpoint                        |
| `pagination.hasNextPage` | Whether more pages exist (only relevant in page mode)                         |
| `pagination.page`        | Current page number (only relevant in page mode)                              |
| `pricing.creditsCharged` | Credits consumed — charged per item returned                                  |

**The `data` fields vary by endpoint** (reviews have `score`, `text`; YouTube results have `title`, `viewCount`, etc.), but the envelope and all other top-level fields are always identical.

Always log `requestId` and `pricing.creditsCharged`:

```python
body = response.json()
print(f"requestId: {body['requestId']}, credits: {body['pricing']['creditsCharged']}")
```

{% endstep %}

{% step %}

## Handle Errors

Errors use the same envelope with `success: false`:

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 400,
  "message": "Validation failed",
  "success": false,
  "error": { "code": "VALIDATION_ERROR" }
}
```

| Status | Code                          | Cause                                  |
| ------ | ----------------------------- | -------------------------------------- |
| `400`  | `VALIDATION_ERROR`            | Invalid parameter name or value        |
| `401`  | `AUTHENTICATION_UNAUTHORIZED` | Missing or invalid API key             |
| `402`  | `TEAM_INSUFFICIENT_CREDITS`   | Credit balance is 0                    |
| `429`  | `RATE_LIMIT_EXCEEDED`         | Request rate exceeded for your plan    |
| `5xx`  | —                             | Server-side error — retry with backoff |

Failed requests do not consume credits.

```python
response = requests.get(url, params=params, headers=headers, timeout=30)

if not response.ok:
    body = response.json()
    print(f"Error {body['status']}: {body['error']['code']}")
    print(f"requestId: {body['requestId']}")
else:
    body = response.json()
    # process body["data"]
```

{% endstep %}

{% step %}

## Paginate Results

All paginated endpoints use the same two fetch modes — choose one per request:

{% tabs %}
{% tab title="Bulk fetch (`maxItems`)" %}
Get up to the endpoint maximum in one call:

```python
body = requests.get(
    "https://api.pullbay.com/appstore/reviews",
    params={"appId": "284882215", "maxItems": 500},
    headers={"Authorization": f"Bearer {os.environ['PULLBAY_API_KEY']}"},
    timeout=120,
).json()

print(f"{len(body['data'])} results — {body['pricing']['creditsCharged']} credits")
```

{% endtab %}

{% tab title="Page-by-page (`page`)" %}
Iterate one page at a time:

```python
all_results = []
page = 1

while True:
    body = requests.get(
        "https://api.pullbay.com/appstore/reviews",
        params={"appId": "284882215", "page": page},
        headers={"Authorization": f"Bearer {os.environ['PULLBAY_API_KEY']}"},
        timeout=30,
    ).json()

    all_results.extend(body["data"])

    if not body["pagination"]["hasNextPage"]:
        break
    page += 1
```

{% endtab %}
{% endtabs %}

`maxItems` and `page` are mutually exclusive — use one or the other, never both.
{% endstep %}
{% endstepper %}

***

## Check Your Usage

In your [dashboard](https://dashboard.pullbay.com/) → **Usage**:

* Credits remaining in your account
* Requests broken down by endpoint and date
* Rate limit status

***

## What's Next?

* **Authentication Guide** — key rotation, security best practices, multiple keys
* **Data Guide** — full field reference for App Store review objects
* **Review Monitoring Guide** — scheduling, alerting, and database storage patterns
* **Integration Guides** — n8n, Google Sheets, and backend integration examples

***

## FAQ

<details>

<summary>How do I find an app's ID?</summary>

For App Store apps, the numeric ID is in the App Store URL:

```
https://apps.apple.com/us/app/instagram/id389801252
                                               ↑
                                     appId: 389801252
```

You can also pass a `bundleId` (e.g. `com.burbn.instagram`) instead of `appId` for App Store and Google Play endpoints.

</details>

<details>

<summary>How many requests do my free credits cover?</summary>

Credits are charged per query and per returned item. A query returning 50 reviews will cost 50 credits plus the cost of the query (1 credit per item). By checking the `pricing.creditsCharged` value in each response, you can see exactly how many credits each call has consumed.

</details>

<details>

<summary>What if I get a <code>401</code> error?</summary>

Your API key is missing or invalid. Confirm the `Authorization: Bearer YOUR_KEY` header is present and the key is active in your dashboard.

</details>

<details>

<summary>Can I use Pullbay from client-side JavaScript?</summary>

No — never expose your API key in browser or mobile code. Build a backend endpoint that adds the key server-side and proxies the response to your frontend.

</details>


# Authentication

Learn how to authenticate your requests to the Pullbay API using Bearer tokens. This guide covers getting your API key, implementing authentication in multiple languages, managing key security, and rotating keys safely.

## Getting Your API Key

Every Pullbay API request requires authentication with an API key.

{% stepper %}
{% step %}

## Sign in

Sign in to your [Pullbay Dashboard](https://dashboard.pullbay.com/)
{% endstep %}

{% step %}

## Open API Keys

Navigate to **API Keys** in the left sidebar
{% endstep %}

{% step %}

## Create a key

Click **Create New Key** and give it a name (e.g. "Production", "Development")
{% endstep %}

{% step %}

## Copy the key

Copy the key
{% endstep %}
{% endstepper %}

**Never share your API keys.** Treat them like passwords.

## How to Authenticate

Every request to the Pullbay API must include your API key in the `Authorization` header using the Bearer scheme:

```
Authorization: Bearer YOUR_API_KEY
```

### cURL

```bash
curl "https://api.pullbay.com/appstore/reviews?appId=284882215&maxItems=5" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Python

```python
import requests
import os

api_key = os.environ["PULLBAY_API_KEY"]

response = requests.get(
    "https://api.pullbay.com/appstore/reviews",
    params={"appId": "284882215", "maxItems": 5},
    headers={"Authorization": f"Bearer {api_key}"},
    timeout=30,
)
print(response.json())
```

### Node.js (Axios)

```javascript
const axios = require('axios');

const apiKey = process.env.PULLBAY_API_KEY;

const response = await axios.get(
  'https://api.pullbay.com/appstore/reviews',
  {
    params: { appId: '284882215', maxItems: 5 },
    headers: { 'Authorization': `Bearer ${apiKey}` },
  }
);

console.log(response.data);
```

### Node.js (Fetch)

```javascript
const apiKey = process.env.PULLBAY_API_KEY;

const params = new URLSearchParams({ appId: '284882215', maxItems: '5' });
const response = await fetch(
  `https://api.pullbay.com/appstore/reviews?${params}`,
  {
    method: 'GET',
    headers: { 'Authorization': `Bearer ${apiKey}` },
  }
);

const data = await response.json();
console.log(data);
```

### PHP

```php
<?php
$apiKey = getenv('PULLBAY_API_KEY');
$url    = 'https://api.pullbay.com/appstore/reviews?appId=284882215&maxItems=5';

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $apiKey,
]);

$response = curl_exec($ch);
$data     = json_decode($response, true);
print_r($data);
```

### Go

```go
package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
)

func main() {
    apiKey := os.Getenv("PULLBAY_API_KEY")
    url    := "https://api.pullbay.com/appstore/reviews?appId=284882215&maxItems=5"

    req, _ := http.NewRequest("GET", url, nil)
    req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiKey))

    client := &http.Client{}
    resp, _ := client.Do(req)
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    fmt.Println(string(body))
}
```

## Authentication Errors

A failed authentication returns `401 Unauthorized`:

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 401,
  "message": "Unauthorized",
  "success": false,
  "error": { "code": "AUTHENTICATION_UNAUTHORIZED" }
}
```

| Cause                          | Solution                                                                   |
| ------------------------------ | -------------------------------------------------------------------------- |
| Missing `Authorization` header | Add `Authorization: Bearer YOUR_API_KEY` to every request                  |
| Wrong header format            | Must be `Authorization: Bearer YOUR_KEY` — capital B, space after "Bearer" |
| API key revoked                | Generate a new key in the dashboard and update your application            |
| API key not yet active         | Wait a few seconds after creation and retry                                |

Failed requests — including `401` — **do not consume credits**.

## API Key Management

### Creating a Key

{% stepper %}
{% step %}

## Go to API Keys

Go to [dashboard.pullbay.com](https://dashboard.pullbay.com/) → **API Keys**
{% endstep %}

{% step %}

## Create a key

Click **Create New Key**
{% endstep %}

{% step %}

## Name the key

Add a descriptive name (e.g. "Mobile App", "Analytics Dashboard", "n8n Workflow")
{% endstep %}

{% step %}

## Copy it

Click **Create** and copy the key immediately
{% endstep %}
{% endstepper %}

### Rotating a Key Safely (Zero Downtime)

{% stepper %}
{% step %}

## Create a new key

Create a new key alongside the old one — both work at the same time
{% endstep %}

{% step %}

## Update applications

Update all applications to use the new key; test in staging first
{% endstep %}

{% step %}

## Deploy and verify

Deploy the changes to all instances and verify requests are succeeding with the new key
{% endstep %}

{% step %}

## Revoke the old key

Revoke the old key once you're confident nothing still uses it — this invalidates it immediately
{% endstep %}
{% endstepper %}

### Revoking a Key

{% stepper %}
{% step %}

## Open API Keys

Go to **API Keys** in the dashboard
{% endstep %}

{% step %}

## Find the key

Find the key you want to revoke
{% endstep %}

{% step %}

## Revoke it

Click the three-dot menu → **Revoke**
{% endstep %}

{% step %}

## Confirm

Confirm — the key is invalidated immediately
{% endstep %}
{% endstepper %}

Any application still using the revoked key will begin receiving `401` responses. Update your code before revoking.

### Viewing Key Usage

Go to **API Keys**, select a key, and view its activity log:

* Total requests made with this key
* Requests by endpoint
* Requests by date
* Credits consumed

## Security Best Practices

### Storage

* **Use environment variables** — never hardcode a key in source code
* **Use a secrets manager** in production (AWS Secrets Manager, Google Secret Manager, HashiCorp Vault)
* **Add `.env` files to `.gitignore`** — keys committed to version control are exposed even if later removed

```python
import os
api_key = os.environ["PULLBAY_API_KEY"]  # Correct — read from environment
api_key = "abc123..."                    # Never do this
```

### Access

* **One key per application** — use separate keys for your mobile app, backend service, n8n workflow, and so on
* **Keep live keys out of development** — use a key scoped to your development environment for local testing
* **Rotate every 90 days** or whenever team membership changes
* **Revoke unused keys** promptly — unused keys are attack surface

### What Not to Do

* **Don't commit keys to Git** — even if you remove them later, they remain in commit history
* **Don't include keys in client-side code** — browser JavaScript is readable to anyone
* **Don't share keys via email, Slack, or chat** — use a password manager or secrets manager
* **Don't log keys** — make sure debug output and log files never capture the `Authorization` header

## Common Questions

<details>

<summary>Can I have multiple API keys?</summary>

Yes — create as many keys as you need. Common patterns:

* One key per application (mobile app, backend service, automation workflow)
* One key per environment (development, staging, production)
* One key per team member for individual usage tracking

Each key is independently revocable without affecting the others.

</details>

<details>

<summary>How do I rotate a key without downtime?</summary>

Create a new key first, update all applications to use it, verify everything works, then revoke the old key. Both keys are valid simultaneously during the transition so there is no gap in service.

</details>

<details>

<summary>What should I do if my API key is compromised?</summary>

Act immediately:

1. Revoke the compromised key from the dashboard
2. Create a new key
3. Update all applications and redeploy
4. Review recent usage in the dashboard for any suspicious requests made while the key was exposed

</details>

<details>

<summary>Can I restrict what endpoints a key can access?</summary>

Yes, you can restrict your API keys on a per-endpoint basis.

</details>


# Create an Account

Get started with Pullbay in less than a minute. Learn how to create your account, access your dashboard, and begin using the data API with free credits.

## Why Create a Pullbay Account?

Pullbay provides instant access to app store data through a powerful, developer-friendly API. With your account, you get:

* **Free API credits** — No credit card required to start exploring
* **Full dashboard access** — Monitor usage, manage keys, and track credits
* **All endpoints unlocked** — Immediate access to app store, youtube, google endpoints , ratings, metadata, and more

## Sign Up Steps

Creating a Pullbay account is simple and takes less than 60 seconds:

{% stepper %}
{% step %}

#### Visit the Dashboard

Go to [https://dashboard.pullbay.com](https://dashboard.pullbay.com/) and click the **Sign Up** button in the top right corner.
{% endstep %}

{% step %}

#### Choose Your Sign-Up Method

Pullbay supports three convenient sign-up options:

**Option A: Email & Password**

* Enter your email address
* Create a secure password (at least 8 characters, mix of letters, numbers, and symbols recommended)
* Click **Create Account**

**Option B: Sign In with Google**

* Click **Sign up with Google**
* Authenticate with your Google account
* Pullbay will create an account using your Google email
  {% endstep %}

{% step %}

#### Verify Your Email

A verification email will be sent to your email address. Check your inbox (and spam folder if needed) and click the verification link to activate your account.

Once verified, your account is fully active and ready to use.
{% endstep %}
{% endstepper %}

## What New Accounts Get

When you sign up, Pullbay automatically grants you:

| Benefit              | Details                                                                        |
| -------------------- | ------------------------------------------------------------------------------ |
| **Free API Credits** | 1,000 credits per month to explore the API (no credit card required)           |
| **All Endpoints**    | Complete access to app store reviews, ratings, metadata, and more              |
| **Dashboard Access** | Full access to API keys, usage analytics, service health, and billing settings |
| **Documentation**    | Complete API reference, guides, and integration examples                       |

All new accounts start on the **Free Tier**, which provides generous limits for development and small-scale projects.

## Your Dashboard Sections

Once logged in, your Pullbay dashboard gives you access to these key sections:

| Section            | Purpose                                                                                                  |
| ------------------ | -------------------------------------------------------------------------------------------------------- |
| **Overview**       | View your credit balance, recent API usage trends, account status, and quick links to common actions     |
| **API Keys**       | Create, view, rotate, and revoke your test and live API keys; manage key permissions and access          |
| **Usage**          | Detailed breakdown of your API requests by endpoint, date, and response status; track credit consumption |
| **Service Health** | Real-time status of all Pullbay API endpoints; incident history and maintenance notifications            |
| **Billing**        | Manage your subscription plan, view invoices, purchase additional credits, and update payment methods    |

## Account Security

Your Pullbay account contains sensitive API keys and usage data. Follow these security best practices:

### Use a Strong Password

If you signed up with email and password:

* Use at least 12 characters with a mix of uppercase, lowercase, numbers, and symbols
* Avoid dictionary words and personal information
* Use a password manager to generate and store your password securely

### Protect Your API Keys

* Store API keys in environment variables, never hardcode them
* Rotate keys periodically (at least quarterly)
* Revoke keys immediately if you suspect compromise
* Use different keys for different applications
* Never commit keys to version control systems

## Next Steps

You're now ready to start using Pullbay! Here's what to do next:

1. **Copy your API key** — Go to the **API Keys** section and copy your test key
2. **Read the** [**Authentication Guide**](/documentation/getting-started/authentication) — Learn about test vs. live keys and best practices
3. **Make Your First API Call** — Follow our [First API Call guide ](/documentation/getting-started/first-api-call)to fetch real data
4. **Explore the API** — Check out the [API Reference](https://docs.pullbay.com/api-reference/) to see all available endpoints
5. **Integrate with Your App** — Use Pullbay to power your analytics, monitoring, or review aggregation

## FAQ

<details>

<summary>Is a credit card required to sign up?</summary>

**No.** Pullbay offers a free tier with 1,000 API credits per month, no credit card required. You can explore the entire API and build proof-of-concept projects without any payment method on file. When you need more credits, you can upgrade your plan or purchase additional credits.

</details>

<details>

<summary>How many free credits do I get?</summary>

**1,000 credits per month.** Each standard API request uses 1 credit. This translates to 1,000 API calls per month on the free tier. Credits refresh on the first day of each month and do not carry over. You can view your current balance and credit usage in your dashboard.

</details>

<details>

<summary>Can I use my Google account to sign up?</summary>

**Yes.** Pullbay supports OAuth authentication through Google. Simply click the corresponding button during sign-up, authenticate with your existing credentials, and your Pullbay account will be created using your email from that service. This is a convenient way to sign up without creating a new password.

</details>

<details>

<summary>What happens if I forget my password?</summary>

**You can reset it easily.** On the login page, click **Forgot Password**, enter your email address, and follow the reset link sent to your inbox. If you signed up with Google , use those services to sign in instead.

</details>

<details>

<summary>How do I upgrade my account to get more credits?</summary>

**Visit the Billing section** in your dashboard. You can either:

* Upgrade to a paid plan for higher monthly credit limits and additional features
* Purchase a one-time credit package to use immediately

See our [Pricing page](https://pullbay.com/pricing) for current plan options.

</details>

<details>

<summary>Is there a way to upgrade my account before exploring the API?</summary>

**Absolutely.** You can explore the API with your free credits first, then upgrade whenever you're ready. There's no commitment or minimum contract. You can also reach out to our support team if you'd like to discuss a custom plan for your use case.

</details>

Start your free account today and begin accessing app store data within seconds!


# First API Call

Learn how to make your first API call to Pullbay and understand the response structure. This hands-on guide shows you real code examples and explains every step.

## Prerequisites

Before making your first API call, ensure you have:

* A Pullbay account (sign up at [https://dashboard.pullbay.com](https://dashboard.pullbay.com/))
* Your API key copied from the API Keys section of your dashboard
* A tool for making HTTP requests (cURL, Postman, programming language, etc.)
* Basic familiarity with JSON responses

## The Example Request

We'll fetch App Store reviews for Facebook (App Store ID: 284882215) to demonstrate a real API call.

* **Endpoint:** `GET https://api.pullbay.com/appstore/reviews`
* **Parameters:** `appId=284882215` (Facebook's iOS App Store ID)
* **Authentication:** Header `Authorization: Bearer YOUR_API_KEY`

Replace `YOUR_API_KEY` with your actual API key.

{% tabs %}
{% tab title="cURL" %}

```bash
curl -X GET "https://api.pullbay.com/appstore/reviews?appId=284882215" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

{% endtab %}

{% tab title="Python (requests library)" %}

```python
import requests

api_key = "YOUR_API_KEY"
url = "https://api.pullbay.com/appstore/reviews"
params = {"appId": "284882215"}
headers = {"Authorization": f"Bearer {api_key}"}

response = requests.get(url, params=params, headers=headers)
data = response.json()
print(data)
```

{% endtab %}

{% tab title="Node.js (Axios)" %}

```javascript
const axios = require('axios');

const apiKey = 'YOUR_API_KEY';
const url = 'https://api.pullbay.com/appstore/reviews';
const params = { appId: '284882215' };
const headers = { 'Authorization': `Bearer ${apiKey}` };

async function getReviews() {
  try {
    const response = await axios.get(url, { params, headers });
    console.log(response.data);
  } catch (error) {
    console.error('Error:', error.response.data);
  }
}

getReviews();
```

{% endtab %}

{% tab title="Node.js (Native Fetch)" %}

```javascript
const apiKey = 'YOUR_API_KEY';
const url = 'https://api.pullbay.com/appstore/reviews?appId=284882215';

const response = await fetch(url, {
  method: 'GET',
  headers: { 'Authorization': `Bearer ${apiKey}` },
});

const data = await response.json();
console.log(data);
```

{% endtab %}

{% tab title="PHP" %}

```php
<?php
$apiKey = 'YOUR_API_KEY';
$appId = '284882215';

$url = 'https://api.pullbay.com/appstore/reviews?appId=' . $appId;

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $apiKey,
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode === 200) {
    $data = json_decode($response, true);
    print_r($data);
} else {
    echo "Error: HTTP $httpCode\n";
    echo "Response: $response\n";
}
?>
```

{% endtab %}
{% endtabs %}

## Understanding the Response

Here's what a successful response from this API call looks like:

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "10293847561",
      "date": "2026-01-15T10:30:00Z",
      "userName": "HappyUser123",
      "userUrl": "https://itunes.apple.com/us/reviews/id1632846422",
      "version": "5.2.1",
      "score": 5,
      "title": "Great app!",
      "text": "This app has changed how I work. The interface is intuitive and features are powerful. Highly recommended!",
      "url": "https://itunes.apple.com/us/review?id=284882215",
      "country": "US"
    },
    {
      "id": "10293847562",
      "date": "2026-01-14T15:45:00Z",
      "userName": "CriticalReviewer",
      "userUrl": "https://itunes.apple.com/us/reviews/id1632846423",
      "version": "5.2.0",
      "score": 3,
      "title": "Good but needs work",
      "text": "The core features are solid and the performance is decent. However, the app could use better error handling and more customization options.",
      "url": "https://itunes.apple.com/us/review?id=284882215",
      "country": "US"
    },
    {
      "id": "10293847563",
      "date": "2026-01-13T08:15:00Z",
      "userName": "BugHunter42",
      "userUrl": "https://itunes.apple.com/us/reviews/id1632846424",
      "version": "5.2.0",
      "score": 2,
      "title": "Crashes on startup",
      "text": "The app keeps crashing when I try to open it on iOS 17. This happened after the last update. Please fix!",
      "url": "https://itunes.apple.com/us/review?id=284882215",
      "country": "US"
    }
  ],
  "pagination": {
    "page": 1,
    "hasNextPage": true,
    "cursor": null,
    "offset": null
  },
  "pricing": {
    "creditsCharged": 4
  }
}
```

## Response Field Explanations

The response contains a top-level envelope plus `data`, `pagination`, and `pricing`.

**Top level**

| Field       | Type    | Description                                                           |
| ----------- | ------- | --------------------------------------------------------------------- |
| `requestId` | String  | Unique identifier for this request (useful for debugging and support) |
| `status`    | Integer | HTTP status code                                                      |
| `message`   | String  | Human-readable status message                                         |
| `success`   | Boolean | Whether the request succeeded                                         |

### data (Array)

Each review object has these fields:

| Field      | Type     | Description                              |
| ---------- | -------- | ---------------------------------------- |
| `id`       | String   | Unique identifier for this review        |
| `date`     | ISO 8601 | Timestamp when the review was posted     |
| `userName` | String   | Username or display name of the reviewer |
| `userUrl`  | String   | URL to the reviewer's profile            |
| `version`  | String   | App version the reviewer was using       |
| `score`    | Integer  | Star rating given by the reviewer (1–5)  |
| `title`    | String   | Review title or headline                 |
| `text`     | String   | Full text of the review                  |
| `url`      | String   | URL to the review                        |
| `country`  | String   | Country code of the review               |

### pagination (Object)

| Field               | Type    | Description                                                                             |
| ------------------- | ------- | --------------------------------------------------------------------------------------- |
| `page`              | Integer | Current page number                                                                     |
| `hasNextPage`       | Boolean | `true` if more pages are available                                                      |
| `cursor` / `offset` | String  | Used only by cursor-/offset-paginated services (e.g. Google Maps); `null` for App Store |

### pricing (Object)

| Field            | Type    | Description                                                                |
| ---------------- | ------- | -------------------------------------------------------------------------- |
| `creditsCharged` | Integer | API credits consumed by this request (1 per request + 1 per item returned) |

## Fetching More Pages

When `pagination.hasNextPage` is `true`, fetch the next page by incrementing `page`:

{% tabs %}
{% tab title="cURL" %}

```bash
curl -X GET "https://api.pullbay.com/appstore/reviews?appId=284882215&page=2" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

{% endtab %}

{% tab title="Python" %}

```python
# Fetch the first page
response1 = requests.get(url, params=params, headers=headers)
data1 = response1.json()

# Check if more results are available
if data1["pagination"]["hasNextPage"]:
    params["page"] = 2
    response2 = requests.get(url, params=params, headers=headers)
    data2 = response2.json()

    all_reviews = data1["data"] + data2["data"]
    print(f"Fetched {len(all_reviews)} reviews total")
```

{% endtab %}

{% tab title="Node.js" %}

```javascript
// Fetch the first page
const response1 = await axios.get(url, {
  params: { appId: '284882215' },
  headers,
});
const data1 = response1.data;

// Check if more results are available
if (data1.pagination.hasNextPage) {
  const response2 = await axios.get(url, {
    params: { appId: '284882215', page: 2 },
    headers,
  });
  const data2 = response2.data;

  const allReviews = [...data1.data, ...data2.data];
  console.log(`Fetched ${allReviews.length} reviews total`);
}
```

{% endtab %}
{% endtabs %}

Alternatively, use `maxItems` instead of `page` to pull a larger set in a single call (the two are mutually exclusive).

## Check Your Usage in the Dashboard

{% stepper %}
{% step %}

## Sign in

Sign in to <https://dashboard.pullbay.com>
{% endstep %}

{% step %}

## Open Overview

Open your **Overview** to see your remaining credits
{% endstep %}

{% step %}

## View Usage

Click **Usage** to see detailed request history: number of requests, breakdown by endpoint, requests over time, and credits consumed
{% endstep %}
{% endstepper %}

## Common Errors & Troubleshooting

All errors return the standard envelope with `success: false` and an `error` object:

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": {
    "code": "BAD_REQUEST"
  }
}
```

**400 Bad Request** — Invalid or missing parameters. Verify required parameters are included (e.g. `appId` or `bundleId`) and values are correct (app IDs are numeric strings, 8–12 digits).

**402 Insufficient Credits** — You've used all your available credits. Check your remaining credits in the Overview, then purchase more or upgrade your plan.

**404 Not Found** — The requested resource (e.g. an app or developer ID) couldn't be found. Verify the identifier.

**401 Unauthorized** — Your API key is missing, invalid, or expired. Verify it's copied correctly and sent as `Authorization: Bearer YOUR_API_KEY`. If compromised, revoke it and create a new one.

**429 Too Many Requests** — You've hit a rate limit. Wait before retrying and implement exponential backoff.

**500 / 503 Server Error** — A server-side error or temporary unavailability. Retry after a few seconds; if it persists, contact support with your `requestId`.

### Error Reference Table

| Status    | Message                    | Action                      |
| --------- | -------------------------- | --------------------------- |
| 400       | Bad request                | Verify request parameters   |
| 402       | Insufficient credits       | Purchase more credits       |
| 404       | Not found                  | Verify the identifier       |
| 401       | Unauthorized               | Check your API key          |
| 429       | Too many requests          | Wait and retry with backoff |
| 500 / 503 | Server error / unavailable | Retry or contact support    |

## Next Steps

Congratulations on making your first Pullbay API call! Here's what to explore next:

{% stepper %}
{% step %}

#### [Pagination Guide](/documentation/concepts/pagination)

Page-based pagination and bulk pulls with `maxItems`
{% endstep %}

{% step %}

#### [Authentication Guide](/documentation/getting-started/authentication)

API keys and security best practices
{% endstep %}

{% step %}

#### [App Store API Reference](/documentation/api-and-references/appstore)

Explore all available endpoints and parameters
{% endstep %}

{% step %}

#### [Error Handling](/documentation/support/troubleshooting)

Implement robust error handling in your application
{% endstep %}
{% endstepper %}

## FAQ

<details>

<summary>How do I find an app's ID?</summary>

iOS App IDs appear in the App Store URL — for `https://apps.apple.com/app/facebook/id284882215`, the ID is `284882215`. You can also pass a `bundleId` (e.g. `com.burbn.instagram`) instead of `appId`.

</details>

<details>

<summary>What does appId 284882215 refer to?</summary>

`284882215` is the official Apple App Store ID for Facebook. We use it as a demonstration because it's a well-known, widely available app with many reviews. You can replace it with any app ID to fetch reviews for other applications.

</details>

<details>

<summary>Can I fetch reviews for apps on different app stores?</summary>

Yes. Different services serve different stores:

* iOS reviews: `GET /api/appstore/reviews` (use the iOS `appId` or `bundleId`)
* Android reviews: `GET /api/google-play/reviews` (use the package name)

Check the API Reference for endpoint-specific documentation.

</details>

<details>

<summary>How often are reviews updated?</summary>

Reviews are refreshed regularly (typically within 24 hours of being posted). Check your dashboard or contact support for the refresh schedule for your account.

</details>

<details>

<summary>What if I make the same request twice?</summary>

You'll get fresh data, not cached results. Each request consumes credits (1 per request + 1 per item returned) and may return different reviews if new ones have been posted. There's no automatic deduplication, so account for that if you make repeated requests for the same data.

</details>

<details>

<summary>Can I filter reviews by rating or date?</summary>

The App Store reviews endpoint doesn't support rating or date filters. It supports `sort` (`recent` or `helpful`), `country`, and pagination via `page` / `maxItems`. Filtering and sorting options vary by endpoint, so check each endpoint's reference page.

</details>


# Plans

Pullbay's plans are designed to scale with your usage. Each plan includes a monthly credit allocation, rate limit tier, and support level. **Specific pricing and credit amounts are available in the** [**Pullbay dashboard**](https://dahsboard.pullbay.com/)**.**

***

## Plan Overview

| Plan        | Best For                                        | Rate Limit          |
| ----------- | ----------------------------------------------- | ------------------- |
| **Free**    | Exploration & testing                           | 10 req/min          |
| **Hobby**   | Best for small side projects & light automation | 20 request / minute |
| **Builder** | Perfect for production use and growing apps     | 40 request / minute |
| **Scale**   | High-volume usage                               | 60 request / minute |

{% hint style="info" %}
Exact credit allocations and pricing are shown in the dashboard. No credit card is required to start with the free tier.
{% endhint %}

***

## What Each Plan Includes

Every Pullbay plan includes:

* **Monthly credits** — A base allocation that refreshes each billing cycle
* **Rate limits** — Maximum requests per minute
* **Full endpoint access** — All services and endpoints available on every plan
* **Support tier** — Response time and channels vary by plan

***

## Free Tier

* New accounts automatically start on the free plan — no credit card required.
* 1,000 credits allocated each month
* Speed limit of 10 requests per minute
* Access to all endpoints for exploration purposes
* Full access to the control panel (usage, service status, API keys)
* The Free Plan is designed for exploration purposes. When you’re ready to move to a production environment, upgrade to a paid plan for higher credit and speed limits.

***

## Paid Plans

### Free

Best for small projects, prototypes, and side projects with modest data needs.

* Monthly credit allocation (renews each billing cycle)
* 10 request / minute
* Email support

### Hobby

Best for production applications with regular, scheduled data fetching.

* Higher monthly credit allocation (renews each billing cycle)
* 20 request / minute
* Priority email support

### Builder

Best for high-volume applications, multiple apps or services, or teams with intensive data needs.

* Large monthly credit allocation (custom options available)
* 40 request / minute
* Priority support

### Scale Plan

Best for high-volume applications, multiple apps or services, or teams with intensive data needs.

* Large monthly credit allocation (custom options available)
* 60 request / minute
* Priority support

For exact pricing and credit allocations for each plan, see the [Billing section of your dashboard](https://pullbay.com/).

***

## Choosing a Plan

| Situation                             | Recommended Plan                       |
| ------------------------------------- | -------------------------------------- |
| Just exploring or testing             | Free                                   |
| Building a side project or prototype  | Hobby                                  |
| Running a production app              | Builder                                |
| High-volume, multiple apps, or a team | Scale                                  |
| Not sure?                             | Start free, upgrade when you need more |

A good rule of thumb: estimate your monthly credit usage first (see the [Cost Optimization guide](/documentation/platform-best-practices/cost-optimization-revisit)), then choose the plan whose allocation fits. You can always add top-up credits or upgrade at any time.

***

## Upgrading & Downgrading

### Upgrading

* Takes effect **immediately**
* New rate limits apply right away
* You receive prorated credit for the remainder of your current period

### Downgrading

* Takes effect at the **start of your next billing cycle**
* Current features and rate limits remain until renewal
* Credit allocation changes at renewal

### Cancellation

You can cancel your plan at any time from **Billing > Plan** in your dashboard. After cancellation:

* You keep full access until the end of your current billing period
* Credits included in the package are reset
* You can continue using credits on the free tier

***

## Rate Limits by Plan

| Plan    | Requests per Minute |
| ------- | ------------------- |
| Free    | 10                  |
| Hobby   | 20                  |
| Builder | 40                  |
| Scale   | 60                  |

When you exceed your rate limit, requests return `429 Too Many Requests` until the window resets. See the [Rate Limits guide ](/documentation/plans-and-usage/rate-limits)for how to handle this in code.

***

## Credits & Plans

Your plan includes a monthly allocation that refreshes at renewal. You can also:

* **Top up at any time** — Add one-time credits without changing your plan
* **Enable auto-reload** — Automatically top up when your balance drops below a threshold

{% hint style="info" %}
Plan credits reset monthly and **do not roll over**. Top-up credits **do not expire**.
{% endhint %}

For a full explanation of how credits work, see the [Credits guide](/documentation/plans-and-usage/credits).

***

## Frequently Asked Questions

<details>

<summary>Can I switch plans at any time?</summary>

Yes. Upgrades take effect immediately; downgrades take effect at the next billing cycle.

</details>

<details>

<summary>What happens if I exceed my plan's rate limit?</summary>

You'll receive a `429 Too Many Requests` response. Requests resume after the rate limit window resets (typically 1 minute). See the [Rate Limits guide](/documentation/plans-and-usage/rate-limits) for handling strategies.

</details>

<details>

<summary>Do unused plan credits roll over?</summary>

No. Monthly plan credits reset at each billing cycle.

</details>

<details>

<summary>Is a credit card required to use the free tier?</summary>

No. You can sign up and start making API calls immediately without providing payment information.

</details>

<details>

<summary>Where can I see exact pricing?</summary>

Exact pricing and credit amounts for each plan are shown in the [Pullbay dashboard](https://dashboard.pullbay.com/) under Billing.

</details>

<details>

<summary>What if I need more than Scale-tier limits?</summary>

Contact the Pullbay team to discuss custom arrangements for very high-volume use cases.

</details>

<details>

<summary>Can I estimate my costs before upgrading?</summary>

Yes — see the [Cost Optimization guide ](/documentation/platform-best-practices/cost-optimization-revisit)for a formula to estimate your monthly credit needs.

</details>

***


# Rate Limits

Learn how Pullbay API rate limits work, how to monitor your usage, and proven strategies to handle rate limits without disrupting your application. This guide covers rate limit headers, best practices, and production-ready code examples.

## How Rate Limits Work

Pullbay implements request rate limiting to ensure fair usage and maintain service stability for all users.

### Rate Limit Basics

* **Scope:** Rate limits are applied **per API key**, not per IP address
* **Time Window:** Rate limits reset on a **per-minute basis** (60-second rolling window)
* **Enforcement:** When you exceed your plan's limit, the API returns a `429 Too Many Requests` status code
* **Charges:** Failed requests due to rate limiting still consume API credits

### What Happens When You Exceed Rate Limits

When you exceed your plan's rate limit:

1. The API returns HTTP status `429 Too Many Requests`
2. An error response is sent with the `code: "rate_limit_exceeded"`
3. Subsequent requests are rejected until the time window resets
4. The `reset` header tells you exactly when limits reset (Unix timestamp)

**Error Response Example:**

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded. You have used 60 requests this minute. Limit: 60. Resets at: 1712973600",
    "request_id": "req_abc123xyz789"
  }
}
```

## Plan-Based Rate Limits

Your rate limit depends on your Pullbay plan. Upgrade your plan to increase your request capacity.

| Plan        | Requests/Min | Best For                                 |
| ----------- | ------------ | ---------------------------------------- |
| **Free**    | 10           | Exploration and testing                  |
| **Hobby**   | 20           | Small side projects and light automation |
| **Builder** | 40           | Production use and growing apps          |
| **Scale**   | 60           | High-volume usage                        |

See the [Pullbay dashboard](https://dashboard.pullbay.com/) or the [Plans page](/documentation/plans-and-usage/plans) for current pricing and limits.

**Example:** With the Scale plan (60 req/min), you can sustain about 1 request per second.

### Checking Your Current Rate Limit

Your API key's rate limit is shown:

* In the Pullbay dashboard under Billing
* In the `limit` response header from any API call

```bash
curl -I https://api.pullbay.com/appstore/reviews/appId=test \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Look for:

```
limit: 60
remaining: 47
resetTime: 1712973600
```

## Rate Limit Response Headers

Every Pullbay API response includes rate limit information in the response headers:

<table><thead><tr><th>Header</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><pre><code>limit
</code></pre></td><td>Integer</td><td>Your plan's maximum requests per minute</td></tr><tr><td><pre><code>remaining
</code></pre></td><td>Integer</td><td>Requests remaining in this minute</td></tr><tr><td><pre><code>resetTime
</code></pre></td><td>Unix Timestamp</td><td>When the current rate limit window resets (seconds since Jan 1, 1970)</td></tr></tbody></table>

**Example Response Headers:**

```
limit: 60
remaining: 43
resetTime: 1712973602
```

**Converting the Unix Timestamp:**

```python
from datetime import datetime
reset_time = datetime.fromtimestamp(1712973602)
print(f"Rate limit resets at: {reset_time}")
# Output: Rate limit resets at: 2024-04-13 10:00:02
```

### Monitoring Rate Limit Status

Always inspect rate limit headers after each request:

```python
response = requests.get(
    "https://api.pullbay.com/appstore/reviews/",
    params={"appId": "com.example.app"},
    headers={"Authorization": f"Bearer {API_KEY}"}
)

remaining = int(response.headers.get("remaining", 0))
limit = int(response.headers.get("limit", 0))
reset = int(response.headers.get("resetTime", 0))

print(f"Requests remaining: {remaining}/{limit}")
print(f"Resets at: {datetime.fromtimestamp(reset)}")
```

## Rate Limit Handling Strategies

### Strategy 1: Proactive Throttling

**Best for:** Predictable, steady-state usage with varying load

Monitor the `remaining` header and proactively slow down requests when approaching the limit. This prevents rate limit errors entirely.

```python
import time
import requests
from datetime import datetime

API_KEY = "abc123xyz789"
BASE_URL = "https://api.pullbay.com"

class ProactiveThrottler:
    def __init__(self, api_key, throttle_threshold=5):
        """
        api_key: Your Pullbay API key
        throttle_threshold: Start throttling when remaining requests drop below this number
        """
        self.api_key = api_key
        self.throttle_threshold = throttle_threshold
        self.last_remaining = None
        self.last_reset = None

    def get_with_throttle(self, endpoint, params):
        """Fetch data with proactive rate limit throttling."""
        headers = {"Authorization": f"Bearer {self.api_key}"}

        response = requests.get(
            f"{BASE_URL}{endpoint}",
            params=params,
            headers=headers
        )

        # Extract rate limit info from headers
        remaining = int(response.headers.get("remaining", 0))
        limit = int(response.headers.get("limit", 0))
        reset = int(response.headers.get("resetTime", 0))

        # Log rate limit status
        print(f"[{datetime.now().isoformat()}] Remaining: {remaining}/{limit}")

        # Proactive throttling: if approaching limit, wait
        if remaining <= self.throttle_threshold:
            time_until_reset = reset - time.time()
            if time_until_reset > 0:
                print(f"⚠️  Approaching rate limit. Waiting {time_until_reset:.1f}s for reset...")
                time.sleep(time_until_reset + 1)

        return response.json()

# Usage
throttler = ProactiveThrottler(api_key=API_KEY, throttle_threshold=10)

reviews = throttler.get_with_throttle(
    "/reviews",
    params={"appId": "com.example.app", "limit": 100}
)
```

**Advantages:**

* Prevents rate limit errors
* Smooth, predictable request flow
* Works well with steady pagination

**Disadvantages:**

* May add unnecessary delays
* Not ideal for bursty traffic patterns

***

### Strategy 2: Reactive Backoff

**Best for:** Simple implementations, occasional rate limit hits

Wait when you encounter a `429` error. Extract the reset time from the `X-RateLimit-Reset` header and retry after the window resets.

```python
import time
import requests
from datetime import datetime

API_KEY = "bc123xyz789"
BASE_URL = "https://api.pullbay.com"

def get_with_reactive_backoff(endpoint, params, max_retries=3):
    """Fetch data with reactive backoff on 429 errors."""
    headers = {"Authorization": f"Bearer {API_KEY}"}

    for attempt in range(max_retries):
        response = requests.get(
            f"{BASE_URL}{endpoint}",
            params=params,
            headers=headers
        )

        if response.status_code == 200:
            return response.json()

        elif response.status_code == 429:
            # Rate limit hit - extract reset time and wait
            reset_timestamp = int(response.headers.get("reset", time.time()))
            wait_seconds = max(reset_timestamp - time.time(), 1)

            print(f"⚠️  Rate limited (429). Waiting {wait_seconds:.1f}s...")
            print(f"Resets at: {datetime.fromtimestamp(reset_timestamp)}")

            time.sleep(wait_seconds + 1)  # Add 1 second buffer
            continue

        else:
            # Non-rate-limit error
            error = response.json().get("error", {})
            raise Exception(f"API Error: {error.get('code')} - {error.get('message')}")

    raise Exception(f"Max retries ({max_retries}) exceeded")

# Usage
reviews = get_with_reactive_backoff(
    "/reviews",
    params={"appId": "com.example.app"}
)
```

**Advantages:**

* Simple to implement
* No overhead if rate limits aren't hit
* Works for occasional bursts

**Disadvantages:**

* Still results in some 429 errors
* Less efficient than proactive throttling
* Cumulative delay when frequently hitting limits

***

### Strategy 3: Exponential Backoff with Jitter

**Best for:** Robust production systems, highly resilient applications

Implement exponential backoff with randomized jitter. When a rate limit (or other transient error) occurs, wait an exponentially increasing duration with random variance to avoid thundering herd problems.

```python
import time
import random
import requests
from datetime import datetime

API_KEY = "abc123xyz789"
BASE_URL = "https://api.pullbay.com/"

class ExponentialBackoffRetry:
    def __init__(self, api_key, max_retries=5, base_delay=1):
        """
        api_key: Your Pullbay API key
        max_retries: Maximum retry attempts
        base_delay: Initial delay in seconds (will be multiplied exponentially)
        """
        self.api_key = api_key
        self.max_retries = max_retries
        self.base_delay = base_delay

    def get_with_exponential_backoff(self, endpoint, params):
        """Fetch data with exponential backoff and jitter."""
        headers = {"Authorization": f"Bearer {self.api_key}"}

        for attempt in range(self.max_retries + 1):
            try:
                response = requests.get(
                    f"{BASE_URL}{endpoint}",
                    params=params,
                    headers=headers,
                    timeout=30
                )

                if response.status_code == 200:
                    print(f"✓ Success on attempt {attempt + 1}")
                    return response.json()

                elif response.status_code == 429:
                    # Rate limit - use X-RateLimit-Reset if available
                    if attempt < self.max_retries:
                        reset_timestamp = int(response.headers.get("reset", 0))
                        if reset_timestamp > 0:
                            wait_seconds = max(reset_timestamp - time.time(), 1)
                            print(f"⚠️  Rate limited (429) on attempt {attempt + 1}. Waiting {wait_seconds:.1f}s...")
                            time.sleep(wait_seconds + 1)
                        else:
                            # Fallback to exponential backoff if reset header missing
                            wait_seconds = self._exponential_backoff_delay(attempt)
                            print(f"⚠️  Rate limited (429) on attempt {attempt + 1}. Exponential backoff {wait_seconds:.1f}s...")
                            time.sleep(wait_seconds)
                        continue
                    else:
                        raise Exception("Max retries exceeded after rate limit")

                elif response.status_code in [500, 502, 503]:
                    # Transient server errors - retry with backoff
                    if attempt < self.max_retries:
                        wait_seconds = self._exponential_backoff_delay(attempt)
                        print(f"⚠️  Server error ({response.status_code}) on attempt {attempt + 1}. Retrying in {wait_seconds:.1f}s...")
                        time.sleep(wait_seconds)
                        continue
                    else:
                        raise Exception(f"Server error {response.status_code} after {self.max_retries} retries")

                else:
                    # Non-retryable error (4xx except 429)
                    error = response.json().get("error", {})
                    raise Exception(f"Non-retryable error: {response.status_code} - {error.get('message')}")

            except requests.Timeout:
                if attempt < self.max_retries:
                    wait_seconds = self._exponential_backoff_delay(attempt)
                    print(f"⚠️  Timeout on attempt {attempt + 1}. Retrying in {wait_seconds:.1f}s...")
                    time.sleep(wait_seconds)
                    continue
                else:
                    raise

        raise Exception("All retry attempts exhausted")

    def _exponential_backoff_delay(self, attempt):
        """Calculate exponential backoff with jitter."""
        # Base formula: 2^attempt * base_delay, with random jitter
        exponential_delay = (2 ** attempt) * self.base_delay
        jitter = random.uniform(0, exponential_delay * 0.1)  # 10% jitter
        return exponential_delay + jitter

# Usage
client = ExponentialBackoffRetry(api_key=API_KEY, max_retries=5, base_delay=1)

reviews = client.get_with_exponential_backoff(
    "/reviews",
    params={"appId": "com.example.app", "limit": 100}
)

print(f"Successfully fetched {len(reviews['data'])} reviews")
```

**Advantages:**

* Most robust approach for production
* Handles transient errors (429, 5xx) gracefully
* Jitter prevents thundering herd
* Exponential backoff allows service recovery time

**Disadvantages:**

* More complex implementation
* Can introduce significant delays during outages

**Backoff Sequence Example:**

```
Attempt 1: Wait 1 + jitter seconds
Attempt 2: Wait 2 + jitter seconds
Attempt 3: Wait 4 + jitter seconds
Attempt 4: Wait 8 + jitter seconds
Attempt 5: Wait 16 + jitter seconds
```

***

## Best Practices for Rate Limit Management

### 1. Monitor Rate Limit Headers After Every Request

Always extract and log rate limit information:

```python
def log_rate_limit_status(response):
    """Log rate limit status from response headers."""
    remaining = response.headers.get("remaining")
    limit = response.headers.get("limit")
    reset = response.headers.get("resetTime")

    print(f"Rate Limit: {remaining}/{limit} (resets at {reset})")
```

### 2. Batch Requests Efficiently

Combine multiple logical requests into fewer API calls when possible:

```python
# ❌ Inefficient: 3 API calls
reviews_us = get_reviews(app_id="com.example", country="us")
reviews_ca = get_reviews(app_id="com.example", country="ca")
reviews_uk = get_reviews(app_id="com.example", country="uk")

# ✓ More efficient: 1 API call with managed pagination
all_reviews = get_reviews(appId="com.example", managed_pagination=True)
```

### 3. Use Managed Pagination Endpoints

Pullbay's managed pagination endpoints (e.g., `/reviews`) handle pagination internally and consume fewer API credits:

```python
# ✓ Better: Uses managed pagination (1 API call for all data)
response = requests.get(
    "https://api.pullbay.com/appstore/reviews",
    params={"appId": "com.example.app"},
    headers={"Authorization": f"Bearer {API_KEY}"}
)
```

### 4. Cache API Responses

Cache responses to avoid redundant API calls:

```python
import json
from datetime import datetime, timedelta

class APICache:
    def __init__(self, ttl_minutes=60):
        self.cache = {}
        self.ttl = timedelta(minutes=ttl_minutes)

    def get(self, cache_key, api_call_func, *args, **kwargs):
        """Get cached result or fetch fresh data."""
        if cache_key in self.cache:
            cached_data, cached_time = self.cache[cache_key]
            if datetime.now() - cached_time < self.ttl:
                print(f"✓ Cache hit for {cache_key}")
                return cached_data

        print(f"⚠️  Cache miss for {cache_key}. Fetching fresh data...")
        fresh_data = api_call_func(*args, **kwargs)
        self.cache[cache_key] = (fresh_data, datetime.now())
        return fresh_data

# Usage
cache = APICache(ttl_minutes=120)

reviews = cache.get(
    "reviews_com.example.app",
    get_reviews,
    app_id="com.example.app"
)
```

### 5. Request Only the Data You Need

Use parameters to limit the amount of data returned:

```python
# ✓ Better: Limit to 50 results
response = requests.get(
    "https://api.pullbay.com/appstore/reviews",
    params={
        "app_id": "com.example.app",
        "limit": 50,  # Get 50 results instead of default 100
        "sort": "date_desc"  # Get newest first
    },
    headers={"Authorization": f"Bearer {API_KEY}"}
)
```

### 6. Implement Request Queuing

For applications that need to make many requests, queue requests and process them at a controlled rate:

```python
import queue
import threading
import time

class RequestQueue:
    def __init__(self, api_key, requests_per_second=5):
        self.api_key = api_key
        self.request_queue = queue.Queue()
        self.results = []
        self.delay = 1 / requests_per_second
        self.running = True

    def enqueue(self, endpoint, params):
        """Add a request to the queue."""
        self.request_queue.put((endpoint, params))

    def worker(self):
        """Process queued requests at controlled rate."""
        while self.running or not self.request_queue.empty():
            try:
                endpoint, params = self.request_queue.get(timeout=1)

                response = requests.get(
                    f"https://api.pullbay.com{endpoint}",
                    params=params,
                    headers={"Authorization": f"Bearer {self.api_key}"}
                )

                self.results.append(response.json())
                self.request_queue.task_done()

                time.sleep(self.delay)  # Rate limiting
            except queue.Empty:
                continue

    def start(self, num_workers=1):
        """Start worker threads."""
        threads = []
        for _ in range(num_workers):
            t = threading.Thread(target=self.worker, daemon=True)
            t.start()
            threads.append(t)
        return threads

# Usage
queue_worker = RequestQueue(api_key=API_KEY, requests_per_second=0.5)
queue_worker.enqueue("/reviews", {"appId": "com.example.app"})
queue_worker.enqueue("/reviews", {"appId": "com.other.app"})
threads = queue_worker.start(num_workers=1)
queue_worker.running = False
```

### 7. Upgrade Your Plan When Needed

If you consistently hit rate limits:

1. **Add bandwidth:** Upgrade to a higher plan for more requests/minute
2. **Monitor trends:** Track rate limit hits in your logs
3. **Optimize queries:** Reduce API calls through caching and batching
4. **Contact sales:** For enterprise scale needs, contact Pullbay sales for custom limits

### 8. Set Up Rate Limit Alerts

Log rate limit status and alert when approaching the limit:

```python
def check_rate_limit_health(remaining, limit):
    """Check rate limit health and alert if necessary."""
    percentage_remaining = (remaining / limit) * 100

    if percentage_remaining < 10:
        print(f"🔴 CRITICAL: {percentage_remaining:.1f}% of requests remaining")
        # Send alert (email, Slack, etc.)
    elif percentage_remaining < 25:
        print(f"🟡 WARNING: {percentage_remaining:.1f}% of requests remaining")
    else:
        print(f"🟢 OK: {percentage_remaining:.1f}% of requests remaining")
```

## Production-Ready Rate Limit Tracking

Here's a complete example tracking rate limit usage over time:

```python
import csv
from datetime import datetime
import requests

class RateLimitTracker:
    def __init__(self, api_key, log_file="rate_limit_log.csv"):
        self.api_key = api_key
        self.log_file = log_file
        self._init_log_file()

    def _init_log_file(self):
        """Initialize CSV log file."""
        try:
            with open(self.log_file, 'a', newline='') as f:
                writer = csv.writer(f)
                # Check if file is empty
                f.seek(0, 2)
                if f.tell() == 0:
                    writer.writerow([
                        "timestamp", "endpoint", "status", "remaining",
                        "limit", "reset_time", "credits_used"
                    ])
        except FileNotFoundError:
            with open(self.log_file, 'w', newline='') as f:
                writer = csv.writer(f)
                writer.writerow([
                    "timestamp", "endpoint", "status", "remaining",
                    "limit", "reset_time", "credits_used"
                ])

    def make_request(self, endpoint, params):
        """Make API request and log rate limit status."""
        response = requests.get(
            f"https://api.pullbay.com/{endpoint}",
            params=params,
            headers={"Authorization": f"Bearer {self.api_key}"}
        )

        # Extract metadata
        remaining = response.headers.get("remaining")
        limit = response.headers.get("limit")
        reset = response.headers.get("resetTime")

        data = response.json()
        credits_used = data.get("meta", {}).get("credits_used", 0)

        # Log to file
        with open(self.log_file, 'a', newline='') as f:
            writer = csv.writer(f)
            writer.writerow([
                datetime.now().isoformat(),
                endpoint,
                response.status_code,
                remaining,
                limit,
                reset,
                credits_used
            ])

        return data

# Usage
tracker = RateLimitTracker(api_key=API_KEY)
reviews = tracker.make_request(
    "/reviews",
    {"appId": "com.example.app"}
)
```

## Frequently Asked Questions

<details>

<summary>What happens when I hit the rate limit?</summary>

You receive an HTTP `429 Too Many Requests` response with the error code `rate_limit_exceeded`. The `resetTime` header tells you when the rate limit window resets (Unix timestamp). Implement backoff logic to retry after the reset time.

</details>

<details>

<summary>Does the rate limit reset every minute?</summary>

Yes. Pullbay uses a **rolling 60-second time window**. The oldest requests from the window drop off as time advances, making room for new requests. The `resetTime` header shows the exact Unix timestamp when your current window resets.

</details>

<details>

<summary>Can I get a higher rate limit?</summary>

Yes:

1. **Upgrade your plan** in the Pullbay dashboard (self-service)
2. **Contact the Pullbay team** for custom arrangements beyond Scale-tier limits

Upgrading takes effect immediately.

</details>

<details>

<summary>Are credits charged for rate-limited requests?</summary>

Yes. Failed requests due to rate limiting (`429` status) still consume API credits.

</details>

<details>

<summary>How do I know my current rate limit?</summary>

Check the `limit` header from any API response, or view your plan details in the Pullbay dashboard.

</details>

<details>

<summary>Should I implement proactive throttling or reactive backoff?</summary>

* **Proactive throttling:** Best for predictable, steady load with variable traffic
* **Reactive backoff:** Simpler, works well for occasional bursts
* **Exponential backoff:** Best practice for production—handles both rate limits and transient server errors

</details>

<details>

<summary>Can I make burst requests above my rate limit?</summary>

No. Rate limits are strictly enforced per API key. Requests exceeding the limit receive `429` responses until the time window resets. Plan your request patterns to stay within your limit.

</details>

<details>

<summary>What's the relationship between rate limits and API credits?</summary>

They're separate systems:

* **Rate limits** control request frequency (requests/minute)
* **API credits** control total API usage (consumed per request, varies by endpoint)

Both constraints must be satisfied. You might hit rate limits first, or run out of credits first, depending on your usage.

</details>


# Billing

Pullbay uses a credit-based billing model designed to give you control over costs. You choose a plan, use credits, and top up as needed. **Exact pricing is available in the** [**Pullbay dashboard**](https://dashboard.pullbay.com/)**.**

***

## Billing Model

1. **Choose a plan** — Each plan includes a monthly credit allocation, rate limit tier, and support level
2. **Use credits** — Requests consume credits based on the data returned
3. **Top up as needed** — Add more credits at any time without changing your plan

For a full explanation of how credits work, see the [Credits guide.](/documentation/plans-and-usage/credits)

***

## Plans

Plans are billed monthly and include:

* A base credit allocation (refreshes each billing cycle)
* A rate limit tier (requests per minute)
* A support tier

Available plans: **Free**, **Starter**, **Growth**, **Scale**.

For specific plan pricing, credit allocations, and feature comparisons, see the [Plans guide ](#plans)or the [Billing section of your dashboard.](https://dashboard.pullbay.com/billing)

***

## Billing Cycle

* Plans renew on the **same day each month**
* Plan credits **refresh at renewal** — unused plan credits do **not** roll over
* Top-up credits (purchased separately) **do not expire**

***

## Adding Credits

You can add credits at any time without changing your plan:

1. Go to **Billing** in your dashboard
2. Click **Add Credits**
3. Choose an amount
4. Complete payment

Credits are added to your account immediately and do not expire.

***

## Payment Methods

Pullbay accepts:

* Credit and debit cards (Visa, Mastercard, American Express)
* PayPal (available in some regions)

Payment information is stored securely and processed by **Stripe**.

***

## Invoices

Invoices are generated for:

* Monthly plan charges
* Credit top-ups

Access invoices from **Billing > Invoices** in your dashboard. Each invoice includes:

* Line items with descriptions
* Tax breakdown (where applicable)
* Payment status

Email receipts are sent automatically after each charge. You can also download receipts directly from the dashboard.

***

## Changing Plans

### Upgrading

* Takes effect **immediately**
* New rate limits apply right away
* You receive prorated credit for the current period

### Downgrading

* Takes effect at the **start of your next billing cycle**
* Current features and rate limits remain until renewal
* Credit allocation changes at renewal

***

## Cancellation

You can cancel your plan at any time:

1. Go to **Billing > Plan**
2. Click **Cancel Plan**
3. Confirm cancellation

After cancellation:

* You keep access until the end of your billing period
* You will lose the credits included in the package
* You can continue using credits on the free tier

***

## Tax

Prices shown may exclude tax. Applicable taxes are calculated at checkout based on:

* Your billing address
* Tax regulations in your jurisdiction

Business customers may be able to provide tax IDs for exemption, depending on their region.

***

## Frequently Asked Questions

<details>

<summary>Do unused plan credits roll over?</summary>

No. Monthly plan credits reset at each billing cycle. Top-up credits (purchased separately) do not expire.

</details>

<details>

<summary>Can I cancel my plan mid-month?</summary>

Yes. You keep access until the end of your current billing period. No partial refunds are issued.

</details>

<details>

<summary>Is billing handled securely?</summary>

Yes. Payment information is processed by Stripe and never stored on Pullbay's servers.

</details>

<details>

<summary>What payment methods are accepted?</summary>

Credit/debit cards (Visa, Mastercard, Amex) and PayPal in some regions.

</details>

<details>

<summary>How do I get a receipt?</summary>

Email receipts are sent automatically after each charge. You can also download them from **Billing > Invoices** in your dashboard.

</details>

<details>

<summary>What happens if my payment fails?</summary>

Your account may be limited if a payment fails. Update your payment method in **Billing > Payment Methods** and contact support if needed.

</details>

<details>

<summary>Can I set a spending limit?</summary>

You can control spending by setting an auto-reload threshold and not enabling auto-reload, which means your credits won't exceed what you've manually purchased. Configure usage alerts in your account settings.

</details>

***

*Related:* [*Credits* ](/documentation/plans-and-usage/credits)*·* [*Plans*](/documentation/plans-and-usage/plans)*·* [*Usage Tracking* ](/documentation/plans-and-usage/plans)*·* [*Cost Optimization*](/documentation/platform-best-practices/cost-optimization-revisit)


# Credits

Unified data API across Apartments.com, Apple App Store, Google Play, DuckDuckGo, Google Maps, Google News, Google Search, Reddit, Instagram, TikTok, Twitter, Yelp, and YouTube.

**Base URL:** `https://api.pullbay.com`

## Authentication

All requests require a Bearer token in the `Authorization` header:

```http
Authorization: Bearer <your-token>
```

## Credit System

Every request consumes credits in two parts:

* **Base cost** — charged per request regardless of results
* **Item cost** — charged per item returned (1 credit/item)

**Formula:** `total = base_cost + (item_count × item_cost)`

**Example:** `GET /apartments/properties` with 1 result returned → `100 + 1 = 101 credits`

### Credit Costs by Service

| Service           | Endpoint             | Base | Per Item |
| ----------------- | -------------------- | ---- | -------- |
| **Apartments**    | Search properties    | 100  | 1        |
|                   | Get property detail  | 50   | 1        |
|                   | Get property images  | 2    | 1        |
|                   | Get property reviews | 2    | 1        |
| **App Store**     | Get app              | 1    | 1        |
|                   | Search apps          | 1    | 1        |
|                   | Similar apps         | 1    | 1        |
|                   | Get reviews          | 1    | 1        |
|                   | Developer apps       | 1    | 1        |
| **DuckDuckGo**    | Web search           | 3    | 1        |
|                   | Image search         | 3    | 1        |
|                   | News search          | 3    | 1        |
|                   | Video search         | 3    | 1        |
| **Google Maps**   | Place search         | 500  | 1        |
|                   | Get place by FID     | 500  | 1        |
|                   | Place reviews        | 160  | 1        |
| **Google News**   | By category          | 4    | 1        |
|                   | Search               | 4    | 1        |
| **Google Play**   | Get app              | 1    | 1        |
|                   | Search apps          | 1    | 1        |
|                   | Similar apps         | 1    | 1        |
|                   | Reviews              | 1    | 1        |
|                   | Permissions          | 1    | 1        |
|                   | Data safety          | 1    | 1        |
| **Google Search** | Web search           | 20   | 1        |
| **Reddit**        | Search posts         | 50   | 1        |
|                   | Get post             | 10   | 1        |
|                   | Post comments        | 30   | 1        |
|                   | Comment replies      | 30   | 1        |
|                   | Get user             | 10   | 1        |
|                   | User posts           | 30   | 1        |
|                   | User comments        | 30   | 1        |
|                   | Get subreddit        | 30   | 1        |
|                   | Subreddit posts      | 30   | 1        |
| **Instagram**     | Resolve user ID      | 50   | 1        |
|                   | Get user by handle   | 100  | 1        |
|                   | Get user by ID       | 100  | 1        |
|                   | User posts           | 50   | 1        |
|                   | Tagged posts         | 150  | 1        |
|                   | User reels           | 50   | 1        |
|                   | User stories         | 50   | 1        |
|                   | Get post by code     | 50   | 1        |
|                   | Post comments        | 75   | 1        |
| **TikTok**        | Get user             | 50   | 1        |
|                   | User posts           | 60   | 1        |
|                   | Get post by ID       | 60   | 1        |
|                   | Post comments        | 30   | 1        |
|                   | Comment replies      | 30   | 1        |
|                   | Search posts         | 60   | 1        |
|                   | Get post by URL      | 30   | 1        |
|                   | Get hashtag          | 60   | 1        |
| **Twitter**       | Search               | 160  | 1        |
|                   | Get tweet            | 4    | 1        |
|                   | Get user by handle   | 40   | 1        |
|                   | Get user about       | 2    | 1        |
|                   | Get user by ID       | 40   | 1        |
|                   | User tweets          | 160  | 1        |
| **Yelp**          | Get place by ID      | 50   | 1        |
|                   | Place reviews        | 40   | 1        |
|                   | Get place by handle  | 50   | 1        |
|                   | Search places        | 50   | 1        |
| **YouTube**       | Get channel          | 150  | 1        |
|                   | Channel videos       | 150  | 1        |
|                   | Channel shorts       | 150  | 1        |
|                   | Channel live streams | 200  | 1        |
|                   | Get video            | 150  | 1        |
|                   | Get short            | 200  | 1        |
|                   | Video comments       | 10   | 1        |
|                   | Search               | 150  | 1        |
|                   | Hashtag videos       | 150  | 1        |
|                   | Trending             | 250  | 1        |

## Pagination

Paginated endpoints support two mutually exclusive modes:

* `page` — 1-indexed page number
* `maxItems` — one-shot bulk pull up to the specified limit
* `cursor` — opaque cursor string for cursor-paginated endpoints
* `offset` — numeric string for offset-paginated endpoints (Google Maps)

Pagination info is included in the response only when relevant fields are non-null:

```json
{
  "pagination": {
    "page": 1,
    "hasNextPage": true,
    "cursor": "...",
    "offset": null
  }
}
```

## Response Envelope

### Success

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [...],
  "pagination": { "page": 1, "hasNextPage": true, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 101 }
}
```

`pagination` is omitted when all fields are null (non-paginated endpoints).

### Error

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 400,
  "message": "Validation failed",
  "success": false,
  "error": { "code": "VALIDATION_ERROR" }
}
```

**HTTP status codes:**

* `400` — Bad request / validation error
* `402` — Insufficient credits
* `404` — Not found

## Apartments.com

### Search Properties

`GET /apartments/properties`

**Cost: 100 credits/request + 1 credit/item**

Search Apartments.com properties by location with optional filters. Use `page` (1-indexed) or `maxItems` for bulk pull (mutually exclusive).

| Parameter  | Type                 | Required | Description                                                |
| ---------- | -------------------- | -------- | ---------------------------------------------------------- |
| `location` | string (2–40)        | Yes      | e.g. `Seattle, WA`                                         |
| `sort`     | enum                 | No       | `default`, `lastUpdated`, `rentHighToLow`, `rentLowToHigh` |
| `minRent`  | number (0–999999999) | No       | Minimum rent filter                                        |
| `maxRent`  | number (0–999999999) | No       | Maximum rent filter                                        |
| `minBed`   | number (0–4)         | No       | Minimum bedroom count                                      |
| `maxBed`   | number (0–4)         | No       | Maximum bedroom count                                      |
| `minBath`  | number (0–3)         | No       | Minimum bathroom count                                     |
| `maxBath`  | number (0–3)         | No       | Maximum bathroom count                                     |
| `maxItems` | integer (1–200)      | No       | Bulk pull limit                                            |
| `page`     | integer (1–100)      | No       | Page number                                                |

### Get Property Detail

`GET /apartments/properties/{propertyId}`

**Cost: 50 credits/request + 1 credit/item**

Returns full details for a property. Use the boolean flags to opt into extra fields.

| Parameter             | Type                 | Required | Description           |
| --------------------- | -------------------- | -------- | --------------------- |
| `propertyId`          | string (path)        | Yes      | e.g. `swv1xf7`        |
| `includeScore`        | `"true"` / `"false"` | No       | Include score data    |
| `includeModelDetails` | `"true"` / `"false"` | No       | Include model details |
| `includeImages`       | `"true"` / `"false"` | No       | Include images        |

### Get Property Images

`GET /apartments/properties/{propertyId}/images`

**Cost: 2 credits/request + 1 credit/item**

Returns photos, floor plans, virtual tours, and extended virtual tours.

| Parameter    | Type          | Required | Description    |
| ------------ | ------------- | -------- | -------------- |
| `propertyId` | string (path) | Yes      | e.g. `swv1xf7` |

### Get Property Reviews

`GET /apartments/properties/{propertyId}/reviews`

**Cost: 2 credits/request + 1 credit/item**

Returns reviews for a property. Use `page` or `maxItems` (mutually exclusive).

| Parameter    | Type            | Required | Description     |
| ------------ | --------------- | -------- | --------------- |
| `propertyId` | string (path)   | Yes      | e.g. `7s2qyhy`  |
| `maxItems`   | integer (1–200) | No       | Bulk pull limit |
| `page`       | integer (1–100) | No       | Page number     |

## Apple App Store

### Get App

`GET /appstore/apps`

**Cost: 1 credit/request + 1 credit/item**

Returns one application. Provide exactly one of `appId` or `bundleId`.

| Parameter  | Type                  | Required | Description                |
| ---------- | --------------------- | -------- | -------------------------- |
| `appId`    | string (`^\d{8,12}$`) | One of   | e.g. `389801252`           |
| `bundleId` | string                | One of   | e.g. `com.burbn.instagram` |
| `country`  | string (2)            | No       | 2-letter country code      |
| `lang`     | string                | No       | Language code e.g. `en`    |

### Search Apps

`GET /appstore/search`

**Cost: 1 credit/request + 1 credit/item**

Search the App Store for applications matching `term`. Use `page` or `maxItems` (mutually exclusive).

| Parameter  | Type            | Required | Description                   |
| ---------- | --------------- | -------- | ----------------------------- |
| `term`     | string (1–255)  | Yes      | Search term e.g. `ai dungeon` |
| `country`  | string (2)      | No       | 2-letter country code         |
| `lang`     | string          | No       | Language code                 |
| `maxItems` | integer (1–200) | No       | Bulk pull limit               |
| `page`     | integer (1–5)   | No       | Page number                   |

### Get Similar Apps

`GET /appstore/similar`

**Cost: 1 credit/request + 1 credit/item**

Returns applications similar to the supplied one. Provide exactly one of `appId` or `bundleId`.

| Parameter  | Type       | Required | Description           |
| ---------- | ---------- | -------- | --------------------- |
| `appId`    | string     | One of   | e.g. `389801252`      |
| `bundleId` | string     | One of   | Bundle identifier     |
| `country`  | string (2) | No       | 2-letter country code |
| `lang`     | string     | No       | Language code         |

### Get App Reviews

`GET /appstore/reviews`

**Cost: 1 credit/request + 1 credit/item**

Returns reviews for one application. Use `page` or `maxItems` (mutually exclusive).

| Parameter  | Type            | Required | Description           |
| ---------- | --------------- | -------- | --------------------- |
| `appId`    | string          | One of   | e.g. `389801252`      |
| `bundleId` | string          | One of   | Bundle identifier     |
| `country`  | string (2)      | No       | 2-letter country code |
| `sort`     | enum            | No       | `recent`, `helpful`   |
| `maxItems` | integer (1–500) | No       | Bulk pull limit       |
| `page`     | integer (1–10)  | No       | Page number           |

### Get Developer Apps

`GET /appstore/developer`

**Cost: 1 credit/request + 1 credit/item**

Returns all applications published by a single developer.

| Parameter     | Type                  | Required | Description           |
| ------------- | --------------------- | -------- | --------------------- |
| `developerId` | string (`^\d{8,12}$`) | Yes      | e.g. `389801255`      |
| `country`     | string (2)            | No       | 2-letter country code |

## DuckDuckGo

### Web Search

`GET /duckduckgo/search`

**Cost: 3 credits/request + 1 credit/item**

Returns web search results for `term`.

| Parameter    | Type           | Required | Description                                               |
| ------------ | -------------- | -------- | --------------------------------------------------------- |
| `term`       | string (1–255) | Yes      | Search query                                              |
| `region`     | enum           | Yes      | e.g. `us-en`, `de-de` — full list in spec                 |
| `safeSearch` | enum           | Yes      | `off`, `moderate`, `strict`                               |
| `offset`     | number (≥0)    | No       | Result offset for pagination                              |
| `time`       | enum           | No       | `a` (any), `d` (day), `w` (week), `m` (month), `y` (year) |

### Image Search

`GET /duckduckgo/images`

**Cost: 3 credits/request + 1 credit/item**

Returns image search results for `term`.

| Parameter    | Type           | Required | Description                 |
| ------------ | -------------- | -------- | --------------------------- |
| `term`       | string (1–255) | Yes      | Search query                |
| `region`     | enum           | Yes      | Region code                 |
| `safeSearch` | enum           | Yes      | `off`, `moderate`, `strict` |
| `offset`     | number (≥0)    | No       | Result offset               |

### News Search

`GET /duckduckgo/news`

**Cost: 3 credits/request + 1 credit/item**

Returns news articles for `term`. Use `time` to limit recency.

| Parameter    | Type           | Required | Description                 |
| ------------ | -------------- | -------- | --------------------------- |
| `term`       | string (1–255) | Yes      | Search query                |
| `region`     | enum           | Yes      | Region code                 |
| `safeSearch` | enum           | Yes      | `off`, `moderate`, `strict` |
| `offset`     | number (≥0)    | No       | Result offset               |
| `time`       | enum           | No       | `a`, `d`, `w`, `m`, `y`     |

### Video Search

`GET /duckduckgo/videos`

**Cost: 3 credits/request + 1 credit/item**

Returns video search results for `term`.

| Parameter    | Type           | Required | Description                 |
| ------------ | -------------- | -------- | --------------------------- |
| `term`       | string (1–255) | Yes      | Search query                |
| `region`     | enum           | Yes      | Region code                 |
| `safeSearch` | enum           | Yes      | `off`, `moderate`, `strict` |
| `offset`     | number (≥0)    | No       | Result offset               |
| `time`       | enum           | No       | `a`, `d`, `w`, `m`, `y`     |

## Google Maps

### Search Places

`GET /google-maps/place/search`

**Cost: 500 credits/request + 1 credit/item**

Search Google Maps for places by free-text query. Use `offset` (numeric string) or `maxItems` (mutually exclusive). Optional geo filters available.

| Parameter      | Type             | Required | Description              |
| -------------- | ---------------- | -------- | ------------------------ |
| `query`        | string (1–200)   | Yes      | e.g. `pizza in New York` |
| `countryCode`  | string (2–5)     | No       | Country filter           |
| `languageCode` | string (2–10)    | No       | Language filter          |
| `latitude`     | string           | No       | Latitude for geo filter  |
| `longitude`    | string           | No       | Longitude for geo filter |
| `zoom`         | string           | No       | Map zoom level           |
| `maxItems`     | integer (1–500)  | No       | Bulk pull limit          |
| `offset`       | string (`^\d+$`) | No       | Numeric offset string    |

### Get Place by FID

`GET /google-maps/place/{fid}`

**Cost: 500 credits/request + 1 credit/item**

Returns full details for a place identified by its FID (e.g. `0x47e671d877937b0f:0xb975fcfa192f84d4`).

| Parameter      | Type          | Required | Description            |
| -------------- | ------------- | -------- | ---------------------- |
| `fid`          | string (path) | Yes      | Google Maps feature ID |
| `countryCode`  | string (2–5)  | No       | Country filter         |
| `languageCode` | string (2–10) | No       | Language filter        |

### Get Place Reviews

`GET /google-maps/place/reviews`

**Cost: 160 credits/request + 1 credit/item**

Returns reviews for a place identified by its full Google Maps URL. Use `cursor` or `maxItems` (mutually exclusive).

| Parameter  | Type                  | Required | Description          |
| ---------- | --------------------- | -------- | -------------------- |
| `placeUrl` | string (URI, 10–1000) | Yes      | Full Google Maps URL |
| `maxItems` | integer (1–500)       | No       | Bulk pull limit      |
| `cursor`   | string                | No       | Pagination cursor    |

## Google News

### Articles by Category

`GET /google-news/category`

**Cost: 4 credits/request + 1 credit/item**

Returns top news articles for a given category. `languageCode` selects locale (e.g. `US:en`).

| Parameter      | Type | Required | Description                                                                                |
| -------------- | ---- | -------- | ------------------------------------------------------------------------------------------ |
| `category`     | enum | Yes      | `business`, `entertainment`, `health`, `latest`, `science`, `sport`, `technology`, `world` |
| `languageCode` | enum | No       | e.g. `US:en`, `GB:en`, `DE:de` — full list in spec                                         |

### Search Articles

`GET /google-news/search`

**Cost: 4 credits/request + 1 credit/item**

Returns articles matching `query`. `resolveUrl=yes` follows Google's redirect to the publisher URL.

| Parameter      | Type            | Required | Description     |
| -------------- | --------------- | -------- | --------------- |
| `query`        | string (1–500)  | Yes      | Search query    |
| `languageCode` | enum            | No       | Locale code     |
| `resolveUrl`   | enum            | No       | `yes`, `no`     |
| `maxItems`     | integer (1–100) | No       | Bulk pull limit |

## Google Play

### Get App

`GET /google-play/apps`

**Cost: 1 credit/request + 1 credit/item**

Returns full details for a single app by its package name.

| Parameter | Type       | Required | Description                  |
| --------- | ---------- | -------- | ---------------------------- |
| `appId`   | string     | Yes      | e.g. `com.instagram.android` |
| `country` | string (2) | No       | 2-letter country code        |
| `lang`    | string     | No       | Language code                |

### Search Apps

`GET /google-play/search`

**Cost: 1 credit/request + 1 credit/item**

Search the Google Play Store for apps. Returns lightweight summaries.

| Parameter | Type           | Required | Description           |
| --------- | -------------- | -------- | --------------------- |
| `search`  | string (1–255) | Yes      | Search query          |
| `price`   | enum           | No       | `all`, `free`, `paid` |
| `country` | string (2)     | No       | 2-letter country code |
| `lang`    | string         | No       | Language code         |

### Get Similar Apps

`GET /google-play/similar`

**Cost: 1 credit/request + 1 credit/item**

Returns apps Google Play surfaces as similar to the supplied `appId`.

| Parameter | Type   | Required | Description                  |
| --------- | ------ | -------- | ---------------------------- |
| `appId`   | string | Yes      | e.g. `com.instagram.android` |

### Get App Reviews

`GET /google-play/reviews`

**Cost: 1 credit/request + 1 credit/item**

Returns reviews for an app. Use `cursor` or `maxItems` (mutually exclusive).

| Parameter  | Type            | Required | Description                       |
| ---------- | --------------- | -------- | --------------------------------- |
| `appId`    | string          | Yes      | Package name                      |
| `country`  | string (2)      | No       | 2-letter country code             |
| `lang`     | string          | No       | Language code                     |
| `sort`     | enum            | No       | `NEWEST`, `RATING`, `HELPFULNESS` |
| `maxItems` | integer (1–500) | No       | Bulk pull limit                   |
| `cursor`   | string          | No       | Pagination cursor                 |

### Get App Permissions

`GET /google-play/permissions`

**Cost: 1 credit/request + 1 credit/item**

Returns the list of Android permissions the app requests, grouped by category.

| Parameter | Type   | Required | Description                  |
| --------- | ------ | -------- | ---------------------------- |
| `appId`   | string | Yes      | e.g. `com.instagram.android` |

### Get Data Safety

`GET /google-play/data-safety`

**Cost: 1 credit/request + 1 credit/item**

Returns the developer-declared Data Safety section: data shared, collected, security practices, and privacy policy URL.

| Parameter | Type   | Required | Description                  |
| --------- | ------ | -------- | ---------------------------- |
| `appId`   | string | Yes      | e.g. `com.instagram.android` |

## Google Search

### Web Search

`GET /google-search/search`

**Cost: 20 credits/request + 1 credit/item**

Returns Google search results for `query`. Use `page` or `maxItems` (mutually exclusive).

| Parameter  | Type            | Required | Description          |
| ---------- | --------------- | -------- | -------------------- |
| `query`    | string (1–1000) | Yes      | Search query         |
| `safe`     | string          | No       | Safe search setting  |
| `hl`       | string          | No       | Language hint        |
| `country`  | string          | No       | Country code         |
| `count`    | `"10"`          | No       | Results per page     |
| `noEncode` | string          | No       | Disable URL encoding |
| `maxItems` | integer (1–200) | No       | Bulk pull limit      |
| `page`     | integer (≥1)    | No       | Page number          |

## Reddit

### Search Posts

`GET /reddit/search/post`

**Cost: 50 credits/request + 1 credit/item**

Search Reddit posts by keyword. Use `cursor` or `maxItems` (mutually exclusive).

| Parameter  | Type            | Required | Description                                  |
| ---------- | --------------- | -------- | -------------------------------------------- |
| `keyword`  | string (1–100)  | Yes      | e.g. `javascript`                            |
| `sort`     | enum            | No       | `relevance`, `hot`, `top`, `new`, `comments` |
| `maxItems` | integer (1–200) | No       | Bulk pull limit                              |
| `cursor`   | string (1–100)  | No       | Pagination cursor                            |

### Get Post

`GET /reddit/post/{id}`

**Cost: 10 credits/request + 1 credit/item**

Returns a single Reddit post by its ID.

| Parameter | Type                | Required | Description    |
| --------- | ------------------- | -------- | -------------- |
| `id`      | string (path, 1–32) | Yes      | e.g. `1qclvp3` |

### Get Post Comments

`GET /reddit/post/{id}/comments`

**Cost: 30 credits/request + 1 credit/item**

Returns the top-level comment tree for a Reddit post.

| Parameter | Type                | Required | Description                                        |
| --------- | ------------------- | -------- | -------------------------------------------------- |
| `id`      | string (path, 1–32) | Yes      | Post ID                                            |
| `sort`    | enum                | No       | `best`, `top`, `new`, `controversial`, `old`, `qa` |

### Get Comment Replies

`GET /reddit/post/{id}/comments/{commentId}/replies`

**Cost: 30 credits/request + 1 credit/item**

Returns replies to a specific comment on a Reddit post.

| Parameter   | Type                | Required | Description                                        |
| ----------- | ------------------- | -------- | -------------------------------------------------- |
| `id`        | string (path, 1–32) | Yes      | Post ID e.g. `1qclvp3`                             |
| `commentId` | string (path, 1–32) | Yes      | Comment ID e.g. `nzj5dke`                          |
| `sort`      | enum                | No       | `best`, `top`, `new`, `controversial`, `old`, `qa` |

### Get User

`GET /reddit/user/{username}`

**Cost: 10 credits/request + 1 credit/item**

Returns a Reddit user profile by username (without `u/`).

| Parameter  | Type                 | Required | Description |
| ---------- | -------------------- | -------- | ----------- |
| `username` | string (path, 1–100) | Yes      | e.g. `spez` |

### Get User Posts

`GET /reddit/user/{username}/posts`

**Cost: 30 credits/request + 1 credit/item**

Returns posts submitted by a Reddit user. Use `cursor` or `maxItems` (mutually exclusive).

| Parameter  | Type                 | Required | Description         |
| ---------- | -------------------- | -------- | ------------------- |
| `username` | string (path, 1–100) | Yes      | Reddit username     |
| `sort`     | enum                 | No       | `hot`, `top`, `new` |
| `maxItems` | integer (1–200)      | No       | Bulk pull limit     |
| `cursor`   | string (1–100)       | No       | Pagination cursor   |

### Get User Comments

`GET /reddit/user/{username}/comments`

**Cost: 30 credits/request + 1 credit/item**

Returns comments authored by a Reddit user. Use `cursor` or `maxItems` (mutually exclusive).

| Parameter  | Type                 | Required | Description         |
| ---------- | -------------------- | -------- | ------------------- |
| `username` | string (path, 1–100) | Yes      | Reddit username     |
| `sort`     | enum                 | No       | `hot`, `top`, `new` |
| `maxItems` | integer (1–200)      | No       | Bulk pull limit     |
| `cursor`   | string (1–100)       | No       | Pagination cursor   |

### Get Subreddit

`GET /reddit/subreddit/{name}`

**Cost: 30 credits/request + 1 credit/item**

Returns a subreddit profile by its name (without `r/`).

| Parameter | Type                 | Required | Description        |
| --------- | -------------------- | -------- | ------------------ |
| `name`    | string (path, 1–100) | Yes      | e.g. `programming` |

### Get Subreddit Posts

`GET /reddit/subreddit/{name}/posts`

**Cost: 30 credits/request + 1 credit/item**

Returns posts from a subreddit. Use `cursor` or `maxItems` (mutually exclusive).

| Parameter  | Type                 | Required | Description         |
| ---------- | -------------------- | -------- | ------------------- |
| `name`     | string (path, 1–100) | Yes      | Subreddit name      |
| `sort`     | enum                 | No       | `hot`, `top`, `new` |
| `maxItems` | integer (1–200)      | No       | Bulk pull limit     |
| `cursor`   | string (1–100)       | No       | Pagination cursor   |

## Instagram

### Resolve User ID

`GET /instagram/userid/{handle}`

**Cost: 50 credits/request + 1 credit/item**

Returns whether the handle is available and, if it exists, the resolved Instagram user ID.

| Parameter | Type                 | Required | Description      |
| --------- | -------------------- | -------- | ---------------- |
| `handle`  | string (path, 1–100) | Yes      | e.g. `instagram` |

### Get User by Handle

`GET /instagram/user/handle/{handle}`

**Cost: 100 credits/request + 1 credit/item**

Returns an Instagram user profile by handle (without `@`).

| Parameter | Type                 | Required | Description      |
| --------- | -------------------- | -------- | ---------------- |
| `handle`  | string (path, 1–100) | Yes      | e.g. `instagram` |

### Get User by ID

`GET /instagram/user/{id}`

**Cost: 100 credits/request + 1 credit/item**

Returns an Instagram user profile by numeric user ID.

| Parameter | Type                 | Required | Description     |
| --------- | -------------------- | -------- | --------------- |
| `id`      | string (path, 1–100) | Yes      | e.g. `25025320` |

### Get User Posts

`GET /instagram/user/{id}/posts`

**Cost: 50 credits/request + 1 credit/item**

Returns posts published by a user. Use `cursor` or `maxItems` (mutually exclusive).

| Parameter  | Type                 | Required | Description       |
| ---------- | -------------------- | -------- | ----------------- |
| `id`       | string (path, 1–100) | Yes      | User ID           |
| `maxItems` | integer (1–200)      | No       | Bulk pull limit   |
| `cursor`   | string               | No       | Pagination cursor |

### Get Tagged Posts

`GET /instagram/user/{id}/posts/tagged`

**Cost: 150 credits/request + 1 credit/item**

Returns posts where the user is tagged. Use `cursor` or `maxItems` (mutually exclusive).

| Parameter  | Type                 | Required | Description       |
| ---------- | -------------------- | -------- | ----------------- |
| `id`       | string (path, 1–100) | Yes      | User ID           |
| `maxItems` | integer (1–200)      | No       | Bulk pull limit   |
| `cursor`   | string               | No       | Pagination cursor |

### Get User Reels

`GET /instagram/user/{id}/reels`

**Cost: 50 credits/request + 1 credit/item**

Returns reels published by a user. Use `cursor` or `maxItems` (mutually exclusive).

| Parameter  | Type                 | Required | Description       |
| ---------- | -------------------- | -------- | ----------------- |
| `id`       | string (path, 1–100) | Yes      | User ID           |
| `maxItems` | integer (1–200)      | No       | Bulk pull limit   |
| `cursor`   | string               | No       | Pagination cursor |

### Get User Stories

`GET /instagram/user/{id}/stories`

**Cost: 50 credits/request + 1 credit/item**

Returns the user's currently-active Instagram stories (non-paginated).

| Parameter | Type                 | Required | Description             |
| --------- | -------------------- | -------- | ----------------------- |
| `id`      | string (path, 1–100) | Yes      | User ID e.g. `25025320` |

### Get Post by Shortcode

`GET /instagram/post/code/{code}`

**Cost: 50 credits/request + 1 credit/item**

Returns a single post by its shortcode (the segment after `/p/` in the URL).

| Parameter | Type                 | Required | Description        |
| --------- | -------------------- | -------- | ------------------ |
| `code`    | string (path, 1–100) | Yes      | e.g. `CzABCdef123` |

### Get Post Comments

`GET /instagram/post/{id}/comments`

**Cost: 75 credits/request + 1 credit/item**

Returns comments on an Instagram post. Use `cursor` or `maxItems` (mutually exclusive).

| Parameter  | Type                 | Required | Description                |
| ---------- | -------------------- | -------- | -------------------------- |
| `id`       | string (path, 1–100) | Yes      | e.g. `3890548995286221889` |
| `maxItems` | integer (1–500)      | No       | Bulk pull limit            |
| `cursor`   | string               | No       | Pagination cursor          |

## TikTok

### Get User by Handle

`GET /tiktok/user/handle/{username}`

**Cost: 50 credits/request + 1 credit/item**

Returns a TikTok user profile by handle (without `@`).

| Parameter  | Type                 | Required | Description   |
| ---------- | -------------------- | -------- | ------------- |
| `username` | string (path, 1–100) | Yes      | e.g. `tiktok` |

### Get User Posts

`GET /tiktok/user/{id}/posts`

**Cost: 60 credits/request + 1 credit/item**

Returns posts published by a TikTok user. Use `cursor` or `maxItems` (mutually exclusive). `region` may bias results toward a specific country.

| Parameter  | Type                 | Required | Description            |
| ---------- | -------------------- | -------- | ---------------------- |
| `id`       | string (path, 1–100) | Yes      | e.g. `107955`          |
| `region`   | string (2–5)         | No       | Country code e.g. `US` |
| `maxItems` | integer (1–200)      | No       | Bulk pull limit        |
| `cursor`   | string               | No       | Pagination cursor      |

### Get Post by ID

`GET /tiktok/post/{id}`

**Cost: 60 credits/request + 1 credit/item**

Returns a single TikTok post by its numeric ID.

| Parameter | Type                | Required | Description                |
| --------- | ------------------- | -------- | -------------------------- |
| `id`      | string (path, 1–50) | Yes      | e.g. `7636131611674692894` |
| `region`  | string (2–5)        | No       | Country code               |

### Get Post Comments

`GET /tiktok/post/{id}/comments`

**Cost: 30 credits/request + 1 credit/item**

Returns comments on a TikTok post. Use `cursor` or `maxItems` (mutually exclusive).

| Parameter  | Type                 | Required | Description       |
| ---------- | -------------------- | -------- | ----------------- |
| `id`       | string (path, 1–100) | Yes      | Post ID           |
| `maxItems` | integer (1–500)      | No       | Bulk pull limit   |
| `cursor`   | string               | No       | Pagination cursor |

### Get Comment Replies

`GET /tiktok/post/{id}/comments/{commentId}/replies`

**Cost: 30 credits/request + 1 credit/item**

Returns replies to a specific TikTok comment. Use `cursor` or `maxItems` (mutually exclusive).

| Parameter   | Type                 | Required | Description                |
| ----------- | -------------------- | -------- | -------------------------- |
| `id`        | string (path, 1–100) | Yes      | Post ID                    |
| `commentId` | string (path, 1–100) | Yes      | e.g. `7636187917325304607` |
| `maxItems`  | integer (1–500)      | No       | Bulk pull limit            |
| `cursor`    | string               | No       | Pagination cursor          |

### Search Posts

`GET /tiktok/post/search`

**Cost: 60 credits/request + 1 credit/item**

Search TikTok posts by keyword. Use `cursor` or `maxItems` (mutually exclusive).

| Parameter   | Type            | Required | Description       |
| ----------- | --------------- | -------- | ----------------- |
| `keyword`   | string (1–100)  | Yes      | e.g. `recipe`     |
| `region`    | string (2–5)    | No       | Country code      |
| `dateRange` | string          | No       | Date range filter |
| `sortType`  | string          | No       | Sort type         |
| `maxItems`  | integer (1–200) | No       | Bulk pull limit   |
| `cursor`    | string          | No       | Pagination cursor |

### Get Post by URL

`GET /tiktok/post`

**Cost: 30 credits/request + 1 credit/item**

Returns a single TikTok post by its full URL.

| Parameter | Type                 | Required | Description           |
| --------- | -------------------- | -------- | --------------------- |
| `url`     | string (URI, 1–2048) | Yes      | Full TikTok video URL |

### Get Hashtag

`GET /tiktok/hashtag/handle/{name}`

**Cost: 60 credits/request + 1 credit/item**

Returns a TikTok hashtag profile by name (without `#`).

| Parameter | Type                 | Required | Description |
| --------- | -------------------- | -------- | ----------- |
| `name`    | string (path, 1–100) | Yes      | e.g. `fyp`  |

## Twitter

### Search

`GET /twitter/search`

**Cost: 160 credits/request + 1 credit/item**

Search tweets by query. Use `cursor` or `maxItems` (mutually exclusive). Default sort is `Latest`.

| Parameter  | Type                 | Required | Description                              |
| ---------- | -------------------- | -------- | ---------------------------------------- |
| `query`    | string (1–100)       | Yes      | Search query                             |
| `sort`     | enum                 | No       | `Top`, `Latest` (default: `Latest`)      |
| `useFirst` | `"true"` / `"false"` | No       | Use first result mode (default: `false`) |
| `maxItems` | integer (1–200)      | No       | Bulk pull limit                          |
| `cursor`   | string               | No       | Pagination cursor                        |

### Get Tweet

`GET /twitter/tweet/{id}`

**Cost: 4 credits/request + 1 credit/item**

Returns a single tweet by its numeric ID.

| Parameter | Type                 | Required | Description |
| --------- | -------------------- | -------- | ----------- |
| `id`      | string (path, 1–100) | Yes      | e.g. `20`   |

### Get User by Handle

`GET /twitter/handle/{handle}`

**Cost: 40 credits/request + 1 credit/item**

Returns a Twitter user by handle (without `@`).

| Parameter | Type                 | Required | Description     |
| --------- | -------------------- | -------- | --------------- |
| `handle`  | string (path, 1–100) | Yes      | e.g. `elonmusk` |

### Get User About

`GET /twitter/handle/{handle}/about`

**Cost: 2 credits/request + 1 credit/item**

Returns extended about/bio details for a Twitter user by handle.

| Parameter | Type                 | Required | Description     |
| --------- | -------------------- | -------- | --------------- |
| `handle`  | string (path, 1–100) | Yes      | e.g. `elonmusk` |

### Get User by ID

`GET /twitter/user/{id}`

**Cost: 40 credits/request + 1 credit/item**

Returns a Twitter user by their numeric user ID.

| Parameter | Type                 | Required | Description     |
| --------- | -------------------- | -------- | --------------- |
| `id`      | string (path, 1–100) | Yes      | e.g. `44196397` |

### Get User Tweets

`GET /twitter/user/{id}/tweets`

**Cost: 160 credits/request + 1 credit/item**

Returns tweets posted by a user. Use `cursor` or `maxItems` (mutually exclusive).

| Parameter  | Type                 | Required | Description             |
| ---------- | -------------------- | -------- | ----------------------- |
| `id`       | string (path, 1–100) | Yes      | User ID e.g. `44196397` |
| `maxItems` | integer (1–200)      | No       | Bulk pull limit         |
| `cursor`   | string               | No       | Pagination cursor       |

## Yelp

### Get Place by ID

`GET /yelp/place/{id}`

**Cost: 50 credits/request + 1 credit/item**

Returns a Yelp place by its Yelp business ID.

| Parameter | Type                 | Required | Description                   |
| --------- | -------------------- | -------- | ----------------------------- |
| `id`      | string (path, 1–200) | Yes      | e.g. `JQNDglh1GHuvHriHAcJ1yg` |

### Get Place Reviews

`GET /yelp/place/{id}/reviews`

**Cost: 40 credits/request + 1 credit/item**

Returns reviews for a Yelp place. Use `page` or `maxItems` (mutually exclusive).

| Parameter  | Type                 | Required | Description      |
| ---------- | -------------------- | -------- | ---------------- |
| `id`       | string (path, 1–200) | Yes      | Yelp business ID |
| `sortBy`   | string (1–50)        | No       | Sort option      |
| `maxItems` | integer (1–200)      | No       | Bulk pull limit  |
| `page`     | integer (1–100)      | No       | Page number      |

### Get Place by Handle

`GET /yelp/place/handle/{handle}`

**Cost: 50 credits/request + 1 credit/item**

Returns a Yelp place by its handle/slug.

| Parameter | Type                 | Required | Description                  |
| --------- | -------------------- | -------- | ---------------------------- |
| `handle`  | string (path, 1–200) | Yes      | e.g. `starbucks-seattle-121` |

### Search Places

`GET /yelp/search`

**Cost: 50 credits/request + 1 credit/item**

Search Yelp places by query and location. Use `page` or `maxItems` (mutually exclusive).

| Parameter  | Type            | Required | Description        |
| ---------- | --------------- | -------- | ------------------ |
| `query`    | string (1–200)  | Yes      | e.g. `coffee`      |
| `location` | string (1–200)  | Yes      | e.g. `Seattle, WA` |
| `sortBy`   | string (1–50)   | No       | Sort option        |
| `maxItems` | integer (1–200) | No       | Bulk pull limit    |
| `page`     | integer (1–100) | No       | Page number        |

## YouTube

### Get Channel

`GET /youtube/channel`

**Cost: 150 credits/request + 1 credit/item**

Returns channel details. Provide exactly one of `id`, `handle`, or `channelUrl`.

| Parameter    | Type                  | Required | Description      |
| ------------ | --------------------- | -------- | ---------------- |
| `id`         | string (1–100)        | One of   | Channel ID       |
| `handle`     | string (1–100)        | One of   | e.g. `@MrBeast`  |
| `channelUrl` | string (URI, 10–1000) | One of   | Full channel URL |

### Get Channel Videos

`GET /youtube/channel/{id}/videos`

**Cost: 150 credits/request + 1 credit/item**

Returns videos uploaded by the channel. Use `cursor` or `maxItems` (mutually exclusive).

| Parameter  | Type                 | Required | Description                     |
| ---------- | -------------------- | -------- | ------------------------------- |
| `id`       | string (path, 1–100) | Yes      | e.g. `UCX6OQ3DkcsbYNE6H8uQQuVA` |
| `maxItems` | integer (1–200)      | No       | Bulk pull limit                 |
| `cursor`   | string               | No       | Pagination cursor               |

### Get Channel Shorts

`GET /youtube/channel/{id}/shorts`

**Cost: 150 credits/request + 1 credit/item**

Returns shorts uploaded by the channel. Use `cursor` or `maxItems` (mutually exclusive).

| Parameter  | Type                 | Required | Description       |
| ---------- | -------------------- | -------- | ----------------- |
| `id`       | string (path, 1–100) | Yes      | Channel ID        |
| `maxItems` | integer (1–200)      | No       | Bulk pull limit   |
| `cursor`   | string               | No       | Pagination cursor |

### Get Channel Live Streams

`GET /youtube/channel/{id}/live-streams`

**Cost: 200 credits/request + 1 credit/item**

Returns live streams (current and past) for the channel. Use `cursor` or `maxItems` (mutually exclusive).

| Parameter  | Type                 | Required | Description       |
| ---------- | -------------------- | -------- | ----------------- |
| `id`       | string (path, 1–100) | Yes      | Channel ID        |
| `maxItems` | integer (1–200)      | No       | Bulk pull limit   |
| `cursor`   | string               | No       | Pagination cursor |

### Get Video

`GET /youtube/video`

**Cost: 150 credits/request + 1 credit/item**

Returns full metadata for a single video by its 11-character video ID.

| Parameter | Type          | Required | Description        |
| --------- | ------------- | -------- | ------------------ |
| `id`      | string (5–20) | Yes      | e.g. `dQw4w9WgXcQ` |
| `type`    | enum          | No       | `shorts`, `video`  |

### Get Short

`GET /youtube/shorts/{id}`

**Cost: 200 credits/request + 1 credit/item**

Returns full metadata for a single short by its video ID.

| Parameter | Type                | Required | Description        |
| --------- | ------------------- | -------- | ------------------ |
| `id`      | string (path, 5–20) | Yes      | e.g. `dQw4w9WgXcQ` |

### Get Video Comments

`GET /youtube/comments`

**Cost: 10 credits/request + 1 credit/item**

Returns top-level comments for a video. Use `cursor` or `maxItems` (mutually exclusive). Default sort is `top`.

| Parameter  | Type            | Required | Description                 |
| ---------- | --------------- | -------- | --------------------------- |
| `id`       | string (5–20)   | Yes      | Video ID e.g. `dQw4w9WgXcQ` |
| `sort`     | enum            | No       | `top`, `latest`             |
| `maxItems` | integer (1–500) | No       | Bulk pull limit             |
| `cursor`   | string          | No       | Pagination cursor           |

### Search YouTube

`GET /youtube/search`

**Cost: 150 credits/request + 1 credit/item**

Search YouTube. `mode` selects what to search for. Use `cursor` or `maxItems` (mutually exclusive).

| Parameter    | Type            | Required | Description                                                                                               |
| ------------ | --------------- | -------- | --------------------------------------------------------------------------------------------------------- |
| `query`      | string (1–100)  | Yes      | Search query                                                                                              |
| `mode`       | enum            | Yes      | `video`, `channel`, `playlist`                                                                            |
| `sortBy`     | enum            | No       | `relevance`, `rating`, `upload_date`, `view_count`                                                        |
| `lang`       | string (2–5)    | No       | Language code                                                                                             |
| `geo`        | string (2–5)    | No       | Country code                                                                                              |
| `duration`   | enum            | No       | `short`, `medium`, `long`                                                                                 |
| `features`   | enum            | No       | `360`, `hd`, `subtitles`, `creative_commons`, `3d`, `live`, `purchased`, `4k`, `location`, `hdr`, `vr180` |
| `uploadDate` | enum            | No       | `hour`, `today`, `week`, `month`, `year`                                                                  |
| `maxItems`   | integer (1–200) | No       | Bulk pull limit                                                                                           |
| `cursor`     | string          | No       | Pagination cursor                                                                                         |

### Get Hashtag Videos

`GET /youtube/hashtag`

**Cost: 150 credits/request + 1 credit/item**

Returns videos tagged with `tag` from a single request (non-paginated).

| Parameter | Type           | Required | Description  |
| --------- | -------------- | -------- | ------------ |
| `tag`     | string (1–100) | Yes      | e.g. `music` |

### Get Trending Videos

`GET /youtube/trending`

**Cost: 250 credits/request + 1 credit/item**

Returns currently trending videos. Use `geo` to target a country.

| Parameter | Type         | Required | Description     |
| --------- | ------------ | -------- | --------------- |
| `geo`     | string (2–5) | No       | e.g. `US`, `GB` |


# Services Overview

Pullbay provides a unified API platform for accessing data from multiple sources through a single, consistent interface.

**Base URL:** `https://api.pullbay.com`\
**Authentication:** `Authorization: Bearer YOUR_API_KEY` (get your key from the dashboard)

## Available Services

| Service       | What you can access                                                          | Endpoints |
| ------------- | ---------------------------------------------------------------------------- | --------- |
| App Store     | iOS app lookup, search, similar apps, reviews, developer apps                | 5         |
| Google Play   | Android app lookup, search, similar apps, reviews, permissions, data safety  | 6         |
| Apartments    | Apartments.com properties, photos, and reviews                               | 4         |
| Google Maps   | Place search, place details, and reviews                                     | 3         |
| Google News   | Articles by category or search query                                         | 2         |
| Google Search | Web search results                                                           | 1         |
| DuckDuckGo    | Web search, images, news, videos                                             | 4         |
| Instagram     | Users, posts, reels, stories, and comments                                   | 9         |
| Reddit        | Posts, comments, users, and subreddits                                       | 9         |
| TikTok        | Users, posts, comments, hashtags, and search                                 | 8         |
| Twitter       | Tweets, users, handles, and search                                           | 6         |
| Yelp          | Places, reviews, and search                                                  | 4         |
| YouTube       | Channels, videos, shorts, live streams, comments, search, trending, hashtags | 10        |

Each service has its own reference page with full request/response schemas and examples.

## Response Format

All endpoints return the same envelope:

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [ /* ... */ ],
  "pagination": { "page": 1, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 2 }
}
```

`data` holds the result(s), `pagination` appears on list/search endpoints, `pricing.creditsCharged` reports the credits spent, and an `error` object is returned on failure.

## Endpoint Types & Pagination

* **Detail endpoints** return a single resource or a fixed set in one call, with no pagination (e.g. app lookup, place details).
* **List & search endpoints** support two mutually exclusive modes on the same request: `page` to fetch one page at a time (fine control, fewer credits), or `maxItems` for a one-shot bulk pull that the API pages through internally (simpler for batch jobs and automation).

## Credits

Each endpoint charges a **per-request cost + 1 credit per item returned**. The per-request amount varies by endpoint and is listed on each endpoint's page (App Store endpoints, for example, cost 1 credit per request + 1 credit per item). Monitor usage in the dashboard.

## Service Status

Operational status for every service is shown in your dashboard:

| Symbol | Status         | Impact                               |
| ------ | -------------- | ------------------------------------ |
| 🟢     | Operational    | No impact                            |
| 🟡     | Degraded       | Slower responses                     |
| 🟠     | Partial outage | Some requests may fail (often `503`) |
| 🔴     | Outage         | All requests fail                    |

For partial outages, implement retry logic with exponential backoff.

## FAQ

<details>

<summary><code>page</code> vs <code>maxItems</code>?</summary>

Use `page` to pull one page at a time with fine control over credits; use `maxItems` to fetch a full set in a single call. They're mutually exclusive.

</details>

<details>

<summary>Do I get historical data?</summary>

Yes — you get available historical data, not just recent items. The amount varies by platform and resource popularity.

</details>

<details>

<summary>Can I use Pullbay with n8n, Make, or Zapier?</summary>

Yes. Any platform that supports HTTP requests or webhooks works. See the n8n Integration Guide.

</details>

## Next Steps

* **Service reference pages** — full schemas and examples for each service
* **API Integration Guide** — integrating Pullbay into your backend
* **n8n Integration Guide** — automated workflows


# Making Requests

Learn how to construct and send HTTP requests to the Pullbay API. This guide covers request formatting, headers, parameters, response structure, and practical examples in multiple languages.

## Base URL

All Pullbay API requests use this base URL:

```
https://api.pullbay.com
```

Append the service path and endpoint to this base URL.

## Request Format

```
GET /{service}/{endpoint}?{parameters}
```

Example:

```
GET /appstore/reviews?appId=389801252&country=us&page=1
```

All current Pullbay endpoints are **GET** requests. Parameters are passed in the query string.

## Required Headers

| Header          | Required | Value                          | Example                              |
| --------------- | -------- | ------------------------------ | ------------------------------------ |
| `Authorization` | Yes      | Bearer token with your API key | `Authorization: Bearer YOUR_API_KEY` |
| `User-Agent`    | Optional | Your application identifier    | `User-Agent: MyApp/1.0`              |

### Authorization header format

```
Authorization: Bearer YOUR_API_KEY
```

Replace `YOUR_API_KEY` with your actual API key from the Pullbay dashboard. Your API key is secret — never share it or commit it to version control.

## Common Parameters

Parameters vary by endpoint; each endpoint's reference page lists its own. The App Store reviews endpoint, for example, accepts:

| Parameter  | Type    | Required                  | Description                                                 |
| ---------- | ------- | ------------------------- | ----------------------------------------------------------- |
| `appId`    | string  | One of `appId`/`bundleId` | App Store app ID (numeric). Pattern `^\d{8,12}$`.           |
| `bundleId` | string  | One of `appId`/`bundleId` | App bundle ID (e.g. `com.burbn.instagram`).                 |
| `country`  | string  | No                        | Two-letter storefront code (e.g. `us`, `gb`, `jp`).         |
| `sort`     | string  | No                        | Sort order: `recent` or `helpful`.                          |
| `page`     | integer | No                        | Page number (1–10). Mutually exclusive with `maxItems`.     |
| `maxItems` | integer | No                        | One-shot bulk pull (1–500). Mutually exclusive with `page`. |

Use `page` to fetch one page at a time, or `maxItems` for a single bulk call — not both.

Example:

```
GET /appstore/reviews?appId=389801252&country=us&sort=recent&page=1
GET /appstore/reviews?appId=389801252&country=us&maxItems=500
```

## Request Examples

### Basic GET request

```bash
curl -X GET "https://api.pullbay.com/appstore/reviews?appId=389801252&country=us" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Paginated request (`page`)

```bash
curl -X GET "https://api.pullbay.com/appstore/reviews?appId=389801252&page=2" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Bulk pull (`maxItems`)

```bash
curl -X GET "https://api.pullbay.com/appstore/reviews?appId=389801252&country=us&maxItems=500" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

## Response Structure

All successful responses follow this consistent JSON structure:

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "13997548250",
      "date": "2026-04-25T19:57:33-07:00",
      "userName": "Catie0012",
      "userUrl": "https://itunes.apple.com/us/reviews/id1632846422",
      "version": "426.0.0",
      "score": 5,
      "title": "How I love your app",
      "text": "An unmatched experience.",
      "url": "https://itunes.apple.com/us/review?id=389801252",
      "country": "US"
    }
  ],
  "pagination": {
    "page": 1,
    "hasNextPage": null,
    "cursor": null,
    "offset": null
  },
  "pricing": {
    "creditsCharged": 2
  }
}
```

### Response fields explained

**Top level**

* `requestId` (string): Unique identifier for this request. Save it for debugging and support.
* `status` (integer): HTTP status code.
* `message` (string): Human-readable status message.
* `success` (boolean): Whether the request succeeded.

**`data` (array)**

* Contains the actual results. Each object's structure depends on the endpoint.
* An empty array `[]` means no matching results (not an error).

**`pagination` (object)** — present on list/search endpoints

* `page` (integer): Current page number.
* `hasNextPage` (boolean): Whether more pages are available.
* `cursor` / `offset` (string): Used only by cursor- or offset-paginated services (e.g. Google Maps); `null` for App Store.

**`pricing` (object)**

* `creditsCharged` (number): Credits consumed by this request.

## Error Response Format

When a request fails, the response keeps the same top-level envelope and adds an `error` object:

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": {
    "code": "BAD_REQUEST"
  }
}
```

* `success` is `false` on failure.
* `status` / `message` describe the failure (e.g. `402 Insufficient credits`, `404 Not found`).
* `error.code` is a machine-readable code for programmatic handling.
* Include `requestId` when contacting Pullbay support for faster debugging.

## Complete Request/Response Example

**Request**

```bash
curl -X GET "https://api.pullbay.com/appstore/reviews?appId=389801252&country=us&maxItems=2" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "User-Agent: MyApp/1.0"
```

**Response (200 OK)**

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "13997548250",
      "date": "2026-04-25T19:57:33-07:00",
      "userName": "Catie0012",
      "version": "426.0.0",
      "score": 5,
      "title": "Perfect productivity tool",
      "text": "This app streamlines my entire workflow. Highly recommended!",
      "country": "US"
    },
    {
      "id": "13997548251",
      "date": "2026-04-24T09:45:00-07:00",
      "userName": "MikeK",
      "version": "426.0.0",
      "score": 4,
      "title": "Good but needs one feature",
      "text": "Overall excellent. Would love to see offline support.",
      "country": "US"
    }
  ],
  "pagination": { "page": 1, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 3 }
}
```

## Language-Specific Examples

### Python

```python
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.pullbay.com"

headers = {
    "Authorization": f"Bearer {API_KEY}",
    "User-Agent": "MyApp/1.0",
}

response = requests.get(
    f"{BASE_URL}/appstore/reviews",
    params={"appId": "389801252", "country": "us", "page": 1},
    headers=headers,
)

if response.ok:
    data = response.json()
    reviews = data["data"]
    print(f"Fetched {len(reviews)} reviews (Request ID: {data['requestId']})")
    print(f"Has next page: {data['pagination']['hasNextPage']}")
    print(f"Credits charged: {data['pricing']['creditsCharged']}")
else:
    body = response.json()
    print(f"Error {body['error']['code']}: {body['message']} (Request ID: {body['requestId']})")
```

### Node.js

```javascript
const API_KEY = "YOUR_API_KEY";
const BASE_URL = "https://api.pullbay.com";

async function fetchReviews() {
  const params = new URLSearchParams({ appId: "389801252", country: "us", page: "1" });
  const response = await fetch(`${BASE_URL}/appstore/reviews?${params}`, {
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "User-Agent": "MyApp/1.0",
    },
  });

  const body = await response.json();
  if (response.ok) {
    console.log(`Fetched ${body.data.length} reviews (Request ID: ${body.requestId})`);
    console.log(`Has next page: ${body.pagination.hasNextPage}`);
  } else {
    console.error(`Error ${body.error.code}: ${body.message} (Request ID: ${body.requestId})`);
  }
}

fetchReviews();
```

### PHP

```php
<?php
$apiKey = "YOUR_API_KEY";
$baseUrl = "https://api.pullbay.com";

$params = http_build_query(["appId" => "389801252", "country" => "us", "page" => 1]);

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => "{$baseUrl}/appstore/reviews?{$params}",
    CURLOPT_HTTPHEADER => ["Authorization: Bearer {$apiKey}", "User-Agent: MyApp/1.0"],
    CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$body = json_decode($response, true);
if ($httpCode === 200) {
    echo "Fetched " . count($body["data"]) . " reviews\n";
    echo "Request ID: " . $body["requestId"] . "\n";
} else {
    echo "Error " . $body["error"]["code"] . ": " . $body["message"] . "\n";
}
?>
```

### Ruby

```ruby
require 'net/http'
require 'json'
require 'uri'

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.pullbay.com"

uri = URI("#{BASE_URL}/appstore/reviews")
uri.query = URI.encode_www_form(appId: "389801252", country: "us", page: 1)

request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{API_KEY}"
request["User-Agent"] = "MyApp/1.0"

http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
response = http.request(request)

body = JSON.parse(response.body)
if response.code == "200"
  puts "Fetched #{body['data'].length} reviews"
  puts "Request ID: #{body['requestId']}"
else
  puts "Error #{body['error']['code']}: #{body['message']}"
end
```

## Frequently Asked Questions

<details>

<summary>What's the difference between `page` and `maxItems`?</summary>

They're mutually exclusive modes on list/search endpoints. Use `page` to fetch one page at a time (max 10 pages for App Store reviews) with fine control over credits. Use `maxItems` (max 500 for App Store reviews) to pull a full set in a single call — simpler for batch jobs. Don't send both.

</details>

<details>

<summary>How do I know if there are more results?</summary>

Check `pagination.hasNextPage`. When it's `true`, request the next page by incrementing `page`.

</details>

<details>

<summary>How do I pass multiple parameters?</summary>

Use the query string: `GET /appstore/reviews?appId=389801252&country=us&sort=recent&page=1`.

</details>

<details>

<summary>Should I include the User-Agent header?</summary>

It's optional but recommended for better diagnostics. Identify your application, e.g. `User-Agent: MyApp/1.0`.

</details>

<details>

<summary>What should I do with the `requestId`?</summary>

Save it from responses, especially errors. Include it when contacting Pullbay support so they can locate your request in the logs.

</details>

<details>

<summary>Can I cache API responses?</summary>

Yes — caching reduces calls and credit consumption. Cache app metadata longer than dynamic data like reviews, and validate freshness with timestamps.

</details>

<details>

<summary>What encoding should I use for special characters in parameters?</summary>

URL-encode query parameters with your language's standard library: Python `urllib.parse.urlencode()`, JavaScript `URLSearchParams`, PHP `http_build_query()`, Ruby `URI.encode_www_form()`.

</details>


# Apartments

The Pullbay Apartments API provides programmatic access to Apartments.com property listings, photos, floor plans, virtual tours, and renter reviews. Integrate rental-market data into your applications, dashboards, and analytics pipelines without scraping or manual lookups.

**Base URL:** `https://api.pullbay.com`

## Overview

The Apartments service lets you:

* Search Apartments.com properties by location, with optional rent, bedroom, and bathroom filters
* Look up full details for a single property by its `propertyId`
* Fetch a property's photos, floor plans, and virtual tours
* Fetch renter reviews for a property, with replies/comments included

All endpoints are read-only `GET` requests against public Apartments.com data — you don't need to manage or own a listing of your own.

## Common Use Cases

**Rental Market Research** — Search a metro area or neighborhood by location to see the current mix of available properties, bed/bath configurations, and rent ranges.

**Competitive Rent Analysis** — Pull rent ranges and unit-level pricing for a set of properties in the same area to benchmark pricing against comparable listings.

**Lead Generation** — Identify properties matching a renter's criteria (location, rent, bed/bath counts) to power a search tool or lead-qualification workflow.

**Listing Enrichment** — Use Get Property and Get Property Images together to pull full descriptions, amenities, floor plans, and virtual tours into your own listing pages.

**Review Monitoring for Property Managers** — Use Get Property Reviews on a schedule to track new renter feedback and management responses over time.

## Endpoints

| Endpoint                                          | Description                   | Pagination          |
| ------------------------------------------------- | ----------------------------- | ------------------- |
| `GET /apartments/properties`                      | Search properties by location | `page` / `maxItems` |
| `GET /apartments/properties/{propertyId}`         | Get a property by ID          | —                   |
| `GET /apartments/properties/{propertyId}/images`  | Get images for a property     | —                   |
| `GET /apartments/properties/{propertyId}/reviews` | Get reviews for a property    | `page` / `maxItems` |

## Search Properties

Search Apartments.com properties by location with optional rent/bed/bath filters.

**Request**

```bash
GET /apartments/properties
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Default | Description                                                       |
| ---------- | ------- | -------- | ------- | ----------------------------------------------------------------- |
| `location` | string  | Yes      | —       | Location to search, 2–40 characters (e.g. `Seattle, WA`)          |
| `sort`     | string  | No       | —       | One of `default`, `lastUpdated`, `rentHighToLow`, `rentLowToHigh` |
| `minRent`  | number  | No       | —       | Minimum monthly rent, 0–999999999                                 |
| `maxRent`  | number  | No       | —       | Maximum monthly rent, 0–999999999                                 |
| `minBed`   | number  | No       | —       | Minimum bedroom count, 0–4                                        |
| `maxBed`   | number  | No       | —       | Maximum bedroom count, 0–4                                        |
| `minBath`  | number  | No       | —       | Minimum bathroom count, 0–3                                       |
| `maxBath`  | number  | No       | —       | Maximum bathroom count, 0–3                                       |
| `page`     | integer | No       | —       | Page number, 1–100. Mutually exclusive with `maxItems`            |
| `maxItems` | integer | No       | —       | One-shot bulk pull, 1–200 results. Mutually exclusive with `page` |

**Example**

```bash
curl -G "https://api.pullbay.com/apartments/properties" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d location="Seattle, WA" \
  -d minRent=1500 \
  -d maxBed=2 \
  -d page=1
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "swv1xf7",
      "name": "110 Roy Apartments",
      "bedRange": "Studio - 2 Beds",
      "rentRange": "$1,500 - $3,500",
      "address": {
        "streetAddress": "110 Roy St",
        "city": "Seattle",
        "state": "WA",
        "postalCode": "98109"
      }
    }
  ],
  "pagination": { "page": 1, "hasNextPage": true, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                                     | Type              | Description                                                       |
| ----------------------------------------- | ----------------- | ----------------------------------------------------------------- |
| `data`                                    | array             | Array of [ApartmentsProperty objects](#apartmentsproperty-object) |
| `pagination.page`                         | integer, nullable | Current page number (only set when using `page`)                  |
| `pagination.hasNextPage`                  | boolean, nullable | Whether another page is available                                 |
| `pagination.cursor` / `pagination.offset` | string, nullable  | Unused by this endpoint — always `null` here                      |
| `pricing.creditsCharged`                  | number            | Credits charged for this request                                  |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. There is no published flat rate.

## Get Property

Returns full details for an Apartments.com property. Use the `includeScore`, `includeModelDetails`, and `includeImages` flags to opt into extra fields.

**Request**

```bash
GET /apartments/properties/{propertyId}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter             | Type                    | Required | Default | Description                                                  |
| --------------------- | ----------------------- | -------- | ------- | ------------------------------------------------------------ |
| `propertyId`          | string                  | Yes      | —       | Apartments.com property ID, 1–20 characters (path parameter) |
| `includeScore`        | string (`true`/`false`) | No       | —       | Include the property's rating in the response                |
| `includeModelDetails` | string (`true`/`false`) | No       | —       | Include detailed floor plan/model data                       |
| `includeImages`       | string (`true`/`false`) | No       | —       | Include photo/floor plan/tour data inline                    |

**Example**

```bash
curl -G "https://api.pullbay.com/apartments/properties/swv1xf7" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d includeScore=true \
  -d includeModelDetails=true
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "swv1xf7",
      "propertyName": "110 Roy Apartments",
      "url": "https://www.apartments.com/110-roy-apartments-seattle-wa/swv1xf7/",
      "propertyWebsite": "https://www.110royapts.example/",
      "lastUpdated": "2026-04-30T10:00:00.000Z",
      "description": "Modern apartments in the heart of Queen Anne.",
      "rating": 4.2,
      "beds": "Studio - 2 Beds",
      "baths": "1 - 2 Baths",
      "sqft": "500 - 1,200 sq ft",
      "baseRent": { "min": 1500, "max": 3500 },
      "totalRent": { "min": 1580, "max": 3620 },
      "location": {
        "streetAddress": "110 Roy St",
        "city": "Seattle",
        "state": "WA",
        "postalCode": "98109"
      },
      "coordinates": { "latitude": 47.6262, "longitude": -122.3521 },
      "contact": { "phone": "(206) 555-0110", "officeHours": "Mon-Fri 9am-6pm" },
      "specials": null,
      "schools": { "elementary": "John Hay Elementary", "district": "Seattle Public Schools" },
      "amenities": [
        { "category": "Unique Amenities", "items": ["In Unit Washer & Dryer", "Rooftop Deck"] }
      ],
      "fees": [
        { "name": "Application Fee", "amount": "$45" }
      ],
      "models": [
        { "name": "Studio A", "beds": 0, "baths": 1, "sqft": 500 }
      ],
      "rentals": [
        { "unit": "204", "rent": 1750, "sqft": 540, "availability": "2026-07-01" }
      ],
      "transportation": [
        { "type": "Transit", "name": "Route 8", "distance": "0.1 mi" }
      ],
      "transitAndPOI": [
        { "type": "Grocery", "name": "QFC Queen Anne", "distance": "0.3 mi" }
      ]
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                                                   |
| ------------------------ | ------ | --------------------------------------------------------------------------------------------- |
| `data`                   | array  | Array containing a single [ApartmentsPropertyDetail object](#apartmentspropertydetail-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                                                              |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. There is no published flat rate.

## Get Property Images

Returns photos, floor plans, virtual tours, and extended virtual tours for a property.

**Request**

```bash
GET /apartments/properties/{propertyId}/images
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter    | Type   | Required | Default | Description                                                  |
| ------------ | ------ | -------- | ------- | ------------------------------------------------------------ |
| `propertyId` | string | Yes      | —       | Apartments.com property ID, 1–20 characters (path parameter) |

**Example**

```bash
curl -G "https://api.pullbay.com/apartments/properties/swv1xf7/images" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "photos": [
        { "url": "https://images1.apartments.com/i2/swv1xf7-1/110-roy-apartments-seattle-wa.jpg", "caption": "Building exterior" }
      ],
      "floorPlans": [
        { "name": "Studio A", "url": "https://images1.apartments.com/i2/swv1xf7-floorplan-1/studio-a.jpg" }
      ],
      "virtualTours": [
        { "name": "Studio A 3D Tour", "url": "https://my.matterport.com/show/?m=example1" }
      ],
      "virtualTourExtended": [
        { "name": "Full Property Walkthrough", "url": "https://my.matterport.com/show/?m=example2" }
      ]
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                                                   |
| ------------------------ | ------ | --------------------------------------------------------------------------------------------- |
| `data`                   | array  | Array containing a single [ApartmentsPropertyImages object](#apartmentspropertyimages-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                                                              |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. There is no published flat rate.

## Get Property Reviews

Returns reviews for an Apartments.com property.

**Request**

```bash
GET /apartments/properties/{propertyId}/reviews
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter    | Type    | Required | Default | Description                                                       |
| ------------ | ------- | -------- | ------- | ----------------------------------------------------------------- |
| `propertyId` | string  | Yes      | —       | Apartments.com property ID, 1–20 characters (path parameter)      |
| `page`       | integer | No       | —       | Page number, 1–100. Mutually exclusive with `maxItems`            |
| `maxItems`   | integer | No       | —       | One-shot bulk pull, 1–200 results. Mutually exclusive with `page` |

**Example**

```bash
curl -G "https://api.pullbay.com/apartments/properties/swv1xf7/reviews" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d page=1
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "key": "r-12345",
      "title": "Loved living here",
      "text": "Great staff and amenities. Highly recommend!",
      "rating": 4.5,
      "submissionDate": "Apr 30, 2026",
      "isOwner": false,
      "positiveFeedbackCount": 3,
      "comments": [
        { "author": "Property Manager", "text": "Thank you for the kind words!" }
      ]
    }
  ],
  "pagination": { "page": 1, "hasNextPage": true, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                                     | Type              | Description                                                                   |
| ----------------------------------------- | ----------------- | ----------------------------------------------------------------------------- |
| `data`                                    | array             | Array of [ApartmentsPropertyReview objects](#apartmentspropertyreview-object) |
| `pagination.page`                         | integer, nullable | Current page number (only set when using `page`)                              |
| `pagination.hasNextPage`                  | boolean, nullable | Whether another page is available                                             |
| `pagination.cursor` / `pagination.offset` | string, nullable  | Unused by this endpoint — always `null` here                                  |
| `pricing.creditsCharged`                  | number            | Credits charged for this request                                              |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. There is no published flat rate.

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                  |
| ------ | ---------------------------------------- |
| `400`  | Bad request — missing/invalid parameters |
| `402`  | Insufficient credits                     |
| `404`  | Property or review set not found         |

## Object Schema

### ApartmentsProperty object

| Field       | Type   | Description                                             |
| ----------- | ------ | ------------------------------------------------------- |
| `id`        | string | Apartments.com property ID                              |
| `name`      | string | Property name                                           |
| `bedRange`  | string | Bedroom range across all units (e.g. `Studio - 2 Beds`) |
| `rentRange` | string | Rent range across all units (e.g. `$1,500 - $3,500`)    |
| `address`   | object | Property address fields                                 |

### ApartmentsPropertyDetail object

| Field             | Type               | Description                                                 |
| ----------------- | ------------------ | ----------------------------------------------------------- |
| `id`              | string             | Apartments.com property ID                                  |
| `propertyName`    | string             | Property name                                               |
| `url`             | string             | Apartments.com listing URL                                  |
| `propertyWebsite` | string             | Property's own website                                      |
| `lastUpdated`     | string (date-time) | When the listing was last updated                           |
| `description`     | string             | Property description                                        |
| `rating`          | number             | Average rating (present when `includeScore=true`)           |
| `beds`            | string             | Bedroom range                                               |
| `baths`           | string             | Bathroom range                                              |
| `sqft`            | string             | Square footage range                                        |
| `baseRent`        | object             | Base rent range                                             |
| `totalRent`       | object             | Total rent range (including estimated fees)                 |
| `location`        | object             | Address fields                                              |
| `coordinates`     | object             | `latitude` / `longitude`                                    |
| `contact`         | object             | Contact info (phone, office hours, etc.)                    |
| `specials`        | object, nullable   | Current leasing specials, if any                            |
| `schools`         | object             | Nearby school info                                          |
| `amenities`       | array              | Amenity categories and items                                |
| `fees`            | array              | Fee names and amounts                                       |
| `models`          | array              | Floor plan models (present when `includeModelDetails=true`) |
| `rentals`         | array              | Individual available units                                  |
| `transportation`  | array              | Nearby transit options                                      |
| `transitAndPOI`   | array              | Nearby transit and points of interest                       |

### ApartmentsPropertyImages object

| Field                 | Type  | Description                      |
| --------------------- | ----- | -------------------------------- |
| `photos`              | array | Property photos                  |
| `floorPlans`          | array | Floor plan images                |
| `virtualTours`        | array | Virtual tour links               |
| `virtualTourExtended` | array | Extended/full virtual tour links |

### ApartmentsPropertyReview object

| Field                   | Type    | Description                                             |
| ----------------------- | ------- | ------------------------------------------------------- |
| `key`                   | string  | Review ID                                               |
| `title`                 | string  | Review title                                            |
| `text`                  | string  | Review body                                             |
| `rating`                | number  | Rating, e.g. out of 5                                   |
| `submissionDate`        | string  | Date the review was submitted (e.g. `Apr 30, 2026`)     |
| `isOwner`               | boolean | Whether the reviewer is the property owner/manager      |
| `positiveFeedbackCount` | integer | Number of "helpful" votes                               |
| `comments`              | array   | Replies to the review (e.g. property manager responses) |

## Finding Property IDs

* A property's `propertyId` is returned by the Search Properties endpoint above — use the `id` field from a search result to look up that property's detail, images, or reviews.
* Either run a location search first to discover IDs, or reuse a `propertyId` you've already obtained from a prior search response.

## Sort and Flag Values

* `sort` (Search Properties) accepts exactly one of: `default`, `lastUpdated`, `rentHighToLow`, `rentLowToHigh`.
* `includeScore`, `includeModelDetails`, and `includeImages` (Get Property) are each typed as a string enum of the literal values `"true"` / `"false"` — pass the string, not a real boolean.

## Python Example

This example searches for properties in a location and paginates through results until `pagination.hasNextPage` is `false` or the page cap (10) is reached:

```python
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.pullbay.com"

def search_all_properties(location, max_pages=10):
    all_properties = []
    page = 1
    while page <= max_pages:
        resp = requests.get(
            f"{BASE_URL}/apartments/properties",
            headers={"Authorization": f"Bearer {API_KEY}"},
            params={"location": location, "sort": "lastUpdated", "page": page},
        )
        resp.raise_for_status()
        body = resp.json()
        all_properties.extend(body["data"])
        if not body.get("pagination", {}).get("hasNextPage"):
            break
        page += 1
    return all_properties

properties = search_all_properties("Seattle, WA")
print(f"Fetched {len(properties)} properties")
```

Each request is billed independently, with the charged amount reported in that response's `pricing.creditsCharged` — there's no separate "fetch everything in one call" endpoint, so a multi-page pull like this makes one request per page.

## FAQ

<details>

<summary>How do I find a property's ID?</summary>

Run a Search Properties request for the location you're interested in, then read the `id` field off any result in `data`. Use that value as the `propertyId` path parameter on the detail, images, and reviews endpoints.

</details>

<details>

<summary>What's the difference between `baseRent` and `totalRent` on Get Property?</summary>

`baseRent` is the property's base rent range; `totalRent` is the rent range including estimated fees. Both are returned as min/max objects.

</details>

<details>

<summary>Can I get a property's photos without calling the images endpoint separately?</summary>

Yes — pass `includeImages=true` on Get Property to receive image data inline, or call Get Property Images directly if you only need photos/floor plans/tours.

</details>

<details>

<summary>How many properties or reviews can I fetch in one call?</summary>

Up to 200 with `maxItems`, or up to 100 pages via `page` (page size isn't fixed by the spec). `page` and `maxItems` are mutually exclusive — use one or the other per request.

</details>

<details>

<summary>Can I filter reviews by rating?</summary>

The API doesn't expose a server-side rating filter for reviews. Fetch the reviews you need and filter client-side on the `rating` field.

</details>

<details>

<summary>What's the credit cost per request?</summary>

It's dynamic — every response includes `pricing.creditsCharged` with the exact amount for that call. There's no fixed published rate per endpoint or per result.

</details>

## Next Steps

* [**API Integration Guide**](broken://pages/68e75ab1a73d266350bd68301be87ff97cad563e) — integrate the Pullbay API into your backend application.
* [**n8n Integration Guide**](broken://pages/4308164455b56bbc694cf74c9768ea689c2fe719) — set up automated workflows using n8n with no coding required.
* [**Services Overview**](broken://pages/0d55376cbcca70097a683a71a593d3077825dbf1) — explore all available Pullbay services.


# Appstore

The Pullbay App Store API provides programmatic access to iOS app metadata, search, similar-app recommendations, reviews, and developer catalogs from the Apple App Store. Integrate App Store data into your applications, dashboards, and analytics pipelines without scraping or manual lookups.

**Base URL:** `https://api.pullbay.com`

## Overview

The App Store service lets you:

* Look up a single app by its numeric App ID or its Bundle ID
* Search the App Store by keyword
* Find apps similar to a given app
* Fetch reviews for an app, sorted by recency or helpfulness
* List every app published by a given developer

All endpoints are read-only `GET` requests against public App Store data — you don't need to own or register an app of your own.

## Common Use Cases

**Competitor Analysis** — Pull a competitor's app metadata and reviews to track positioning, version cadence, and rating trends over time.

**Sentiment Monitoring** — Collect reviews on a schedule and feed the `title`/`text` fields into your own sentiment-analysis or alerting pipeline.

**Market Research** — Use Search Apps and Get Similar Apps together to map out a category and see how apps cluster around features and audiences.

**Product Development** — Mine review text for recurring feature requests and complaints to help prioritize a roadmap.

**Developer Portfolio Tracking** — Use Get Developer Apps to see everything a given publisher has shipped, useful for due diligence or competitive landscape work.

## Endpoints

| Endpoint                  | Description                             | Pagination          |
| ------------------------- | --------------------------------------- | ------------------- |
| `GET /appstore/apps`      | Get a single app by App ID or Bundle ID | —                   |
| `GET /appstore/search`    | Search apps by term                     | `page` / `maxItems` |
| `GET /appstore/similar`   | Get apps similar to a given app         | —                   |
| `GET /appstore/reviews`   | Get reviews for an app                  | `page` / `maxItems` |
| `GET /appstore/developer` | Get all apps published by a developer   | —                   |

***

### Get App

Returns a single application. Provide exactly one of `appId` or `bundleId`.

**Request**

```bash
GET /appstore/apps
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type   | Required                  | Default | Description                                                                                       |
| ---------- | ------ | ------------------------- | ------- | ------------------------------------------------------------------------------------------------- |
| `appId`    | string | One of `appId`/`bundleId` | —       | Numeric App Store ID, 8–12 digits (e.g. `389801252`)                                              |
| `bundleId` | string | One of `appId`/`bundleId` | —       | Bundle identifier, up to 255 characters (e.g. `com.burbn.instagram`)                              |
| `country`  | string | No                        | —       | 2-letter storefront code. Not restricted to a fixed list — any 2-letter code is accepted.         |
| `lang`     | string | No                        | —       | Language code for localized text, 2–3 letters with an optional region subtag (e.g. `en`, `en-us`) |

**Example**

```bash
curl -G "https://api.pullbay.com/appstore/apps" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d appId=389801252 \
  -d country=us
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": 389801252,
      "appId": "com.burbn.instagram",
      "title": "Instagram",
      "url": "https://apps.apple.com/us/app/instagram/id389801252?uo=4",
      "description": "Little moments lead to big friendships. Share yours on Instagram.",
      "icon": "https://is1-ssl.mzstatic.com/.../512x512bb.jpg",
      "genres": ["Photo & Video", "Social Networking"],
      "genreIds": ["6008", "6005"],
      "primaryGenre": "Photo & Video",
      "primaryGenreId": 6008,
      "contentRating": "12+",
      "languages": ["EN", "FR", "DE", "JA", "ES"],
      "size": "563586048",
      "requiredOsVersion": "16.3",
      "released": "2010-10-06T08:12:41Z",
      "updated": "2026-04-20T17:48:35Z",
      "releaseNotes": "Performance optimizations and stability improvements.",
      "version": "426.0.0",
      "price": 0,
      "currency": "USD",
      "free": true,
      "developerId": 389801255,
      "developer": "Instagram, Inc.",
      "developerUrl": "https://apps.apple.com/us/developer/instagram-inc/id389801255?uo=4",
      "developerWebsite": "http://instagram.com/",
      "score": 4.69066,
      "reviews": 28962365,
      "currentVersionScore": 4.69066,
      "currentVersionReviews": 28962365,
      "screenshots": ["https://is1-ssl.mzstatic.com/.../392x696bb.jpg"],
      "ipadScreenshots": [],
      "appletvScreenshots": [],
      "supportedDevices": ["iPhone5s-iPhone5s", "iPadAir-iPadAir", "iPhone16Pro-iPhone16Pro"]
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                         |
| ------------------------ | ------ | ------------------------------------------------------------------- |
| `data`                   | array  | Array containing a single [Application object](#application-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                                    |

**Credit cost:** charged dynamically per request; the exact amount is always returned in `pricing.creditsCharged`. There is no published flat rate — read the figure from the response.

***

### Search Apps

Search the App Store for apps matching a term.

**Request**

```bash
GET /appstore/search
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Default | Description                                                       |
| ---------- | ------- | -------- | ------- | ----------------------------------------------------------------- |
| `term`     | string  | Yes      | —       | Search term, 1–255 characters                                     |
| `country`  | string  | No       | —       | 2-letter storefront code                                          |
| `lang`     | string  | No       | —       | Language code for localized text                                  |
| `page`     | integer | No       | —       | Page number, 1–5. Mutually exclusive with `maxItems`              |
| `maxItems` | integer | No       | —       | One-shot bulk pull, 1–200 results. Mutually exclusive with `page` |

**Example**

```bash
curl -G "https://api.pullbay.com/appstore/search" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d term="photo editor" \
  -d country=us \
  -d page=1
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    { "id": 389801252, "appId": "com.burbn.instagram", "title": "Instagram", "...": "..." }
  ],
  "pagination": { "page": 1, "hasNextPage": true, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                                     | Type              | Description                                         |
| ----------------------------------------- | ----------------- | --------------------------------------------------- |
| `data`                                    | array             | Array of [Application objects](#application-object) |
| `pagination.page`                         | integer, nullable | Current page number (only set when using `page`)    |
| `pagination.hasNextPage`                  | boolean, nullable | Whether another page is available                   |
| `pagination.cursor` / `pagination.offset` | string, nullable  | Unused by this endpoint — always `null` here        |
| `pricing.creditsCharged`                  | number            | Credits charged for this request                    |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No flat per-page or per-result rate is published — read the figure from each response.

***

### Get Similar Apps

Returns apps similar to a given app. Provide exactly one of `appId` or `bundleId`.

**Request**

```bash
GET /appstore/similar
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type   | Required                  | Default | Description                       |
| ---------- | ------ | ------------------------- | ------- | --------------------------------- |
| `appId`    | string | One of `appId`/`bundleId` | —       | Numeric App Store ID, 8–12 digits |
| `bundleId` | string | One of `appId`/`bundleId` | —       | Bundle identifier                 |
| `country`  | string | No                        | —       | 2-letter storefront code          |
| `lang`     | string | No                        | —       | Language code for localized text  |

**Example**

```bash
curl -G "https://api.pullbay.com/appstore/similar" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d bundleId=com.burbn.instagram
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    { "id": 454638411, "appId": "com.toyopagroup.picaboo", "title": "Snapchat", "...": "..." }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                         |
| ------------------------ | ------ | --------------------------------------------------- |
| `data`                   | array  | Array of [Application objects](#application-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                    |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get Reviews

Returns reviews for one app. Provide exactly one of `appId` or `bundleId`.

**Request**

```bash
GET /appstore/reviews
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required                  | Default | Description                                                       |
| ---------- | ------- | ------------------------- | ------- | ----------------------------------------------------------------- |
| `appId`    | string  | One of `appId`/`bundleId` | —       | Numeric App Store ID, 8–12 digits                                 |
| `bundleId` | string  | One of `appId`/`bundleId` | —       | Bundle identifier                                                 |
| `country`  | string  | No                        | —       | 2-letter storefront code                                          |
| `sort`     | string  | No                        | —       | `recent` or `helpful`                                             |
| `page`     | integer | No                        | —       | Page number, 1–10. Mutually exclusive with `maxItems`             |
| `maxItems` | integer | No                        | —       | One-shot bulk pull, 1–500 results. Mutually exclusive with `page` |

**Example**

```bash
curl -G "https://api.pullbay.com/appstore/reviews" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d appId=389801252 \
  -d country=us \
  -d sort=recent \
  -d page=1
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "13997548250",
      "date": "2026-04-25T19:57:33-07:00",
      "userName": "Catie0012",
      "userUrl": "https://itunes.apple.com/us/reviews/id1632846422",
      "version": "426.0.0",
      "score": 5,
      "title": "How I love your app",
      "text": "Una experiencia inigualable",
      "url": "https://itunes.apple.com/us/review?id=389801252&type=Purple%20Software",
      "country": "US"
    }
  ],
  "pagination": { "page": 1, "hasNextPage": true, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type              | Description                                      |
| ------------------------ | ----------------- | ------------------------------------------------ |
| `data`                   | array             | Array of [Review objects](#review-object)        |
| `pagination.page`        | integer, nullable | Current page number (only set when using `page`) |
| `pagination.hasNextPage` | boolean, nullable | Whether another page is available                |
| `pricing.creditsCharged` | number            | Credits charged for this request                 |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No flat per-page or per-review rate is published.

***

### Get Developer Apps

Returns the list of apps published by a single developer.

**Request**

```bash
GET /appstore/developer
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter     | Type   | Required | Default | Description                       |
| ------------- | ------ | -------- | ------- | --------------------------------- |
| `developerId` | string | Yes      | —       | Numeric developer ID, 8–12 digits |
| `country`     | string | No       | —       | 2-letter storefront code          |

**Example**

```bash
curl -G "https://api.pullbay.com/appstore/developer" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d developerId=389801255
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    { "id": 389801252, "appId": "com.burbn.instagram", "title": "Instagram", "...": "..." }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                         |
| ------------------------ | ------ | --------------------------------------------------- |
| `data`                   | array  | Array of [Application objects](#application-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                    |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                                                           |
| ------ | --------------------------------------------------------------------------------- |
| `400`  | Bad request — missing/invalid parameters, or both `appId` and `bundleId` supplied |
| `402`  | Insufficient credits                                                              |
| `404`  | App, developer, or review set not found                                           |

## Object Schema

### Application object

| Field                                                    | Type                      | Description                                     |
| -------------------------------------------------------- | ------------------------- | ----------------------------------------------- |
| `id`                                                     | integer                   | Numeric App Store ID                            |
| `appId`                                                  | string                    | Bundle identifier                               |
| `title`                                                  | string                    | App name                                        |
| `url`                                                    | string                    | App Store listing URL                           |
| `description`                                            | string                    | App description                                 |
| `icon`                                                   | string                    | Icon image URL                                  |
| `genres` / `genreIds`                                    | array                     | Genre names and IDs                             |
| `primaryGenre` / `primaryGenreId`                        | string / integer          | Primary genre name and ID                       |
| `contentRating`                                          | string                    | Age rating (e.g. `12+`)                         |
| `languages`                                              | array                     | Supported language codes                        |
| `size`                                                   | string                    | App size in bytes                               |
| `requiredOsVersion`                                      | string                    | Minimum iOS version                             |
| `released` / `updated`                                   | string (date-time)        | Initial release and last update timestamps      |
| `releaseNotes`                                           | string                    | "What's New" text for the current version       |
| `version`                                                | string                    | Current version number                          |
| `price` / `currency` / `free`                            | number / string / boolean | Pricing info                                    |
| `developerId` / `developer`                              | integer / string          | Developer ID and name                           |
| `developerUrl` / `developerWebsite`                      | string                    | Developer's App Store page and external site    |
| `score` / `reviews`                                      | number / integer          | All-time rating and review count                |
| `currentVersionScore` / `currentVersionReviews`          | number / integer          | Rating and review count for the current version |
| `screenshots` / `ipadScreenshots` / `appletvScreenshots` | array                     | Screenshot URLs per device class                |
| `supportedDevices`                                       | array                     | Compatible device identifiers                   |

### Review object

| Field      | Type               | Description                                 |
| ---------- | ------------------ | ------------------------------------------- |
| `id`       | string             | Review ID                                   |
| `date`     | string (date-time) | When the review was posted                  |
| `userName` | string             | Reviewer's display name                     |
| `userUrl`  | string             | Link to the reviewer's profile              |
| `version`  | string             | App version the review was left for         |
| `score`    | integer            | Rating, 1–5                                 |
| `title`    | string             | Review title                                |
| `text`     | string             | Review body                                 |
| `url`      | string             | Permalink to the review                     |
| `country`  | string             | Storefront country the review was posted on |

Note: the Review object does not include a helpful-vote count or "verified purchase" flag — only the fields listed above are returned.

## Finding App IDs and Bundle IDs

* **App ID**: the numeric ID in an App Store URL, e.g. `https://apps.apple.com/us/app/instagram/id389801252` → `389801252`.
* **Bundle ID**: the package identifier shown on a developer's site or in App Store Connect, e.g. `com.burbn.instagram`.
* Either ID resolves the same app — use whichever you already have on hand. Don't pass both in the same request.

## Country and Language Codes

* `country` accepts any 2-letter code (the schema only constrains it to exactly 2 characters) — it isn't validated against a fixed allow-list in the spec, so pass a standard ISO 3166-1 alpha-2 storefront code (`us`, `gb`, `jp`, `de`, ...).
* `lang` accepts a 2–3 letter language code, optionally with a region subtag (`en`, `en-us`, `pt-br`) — also not restricted to a fixed list.
* Neither parameter has a documented default in the API spec. If you omit them, behavior is determined by the underlying App Store lookup rather than a guaranteed Pullbay default — pass both explicitly if you need a predictable storefront/locale.

## Python Example: Paginating Through Reviews

This example fetches reviews for an app a page at a time, stopping when `pagination.hasNextPage` is `false` or the page cap (10) is reached:

```python
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.pullbay.com"

def get_all_reviews(app_id, country="us", max_pages=10):
    all_reviews = []
    page = 1
    while page <= max_pages:
        resp = requests.get(
            f"{BASE_URL}/appstore/reviews",
            headers={"Authorization": f"Bearer {API_KEY}"},
            params={"appId": app_id, "country": country, "sort": "recent", "page": page},
        )
        resp.raise_for_status()
        body = resp.json()
        all_reviews.extend(body["data"])
        if not body.get("pagination", {}).get("hasNextPage"):
            break
        page += 1
    return all_reviews

reviews = get_all_reviews(389801252)
print(f"Fetched {len(reviews)} reviews")
```

Each request is billed independently, with the charged amount reported in that response's `pricing.creditsCharged` — there's no separate "fetch everything in one call" endpoint, so a multi-page pull like this makes one request per page.

## FAQ

<details>

<summary>How do I find an app's ID?</summary>

Visit the app's page on the Apple App Store. Look at the URL — it contains `/id` followed by a number. That number is the `appId`. For example, `https://apps.apple.com/us/app/instagram/id389801252` → `389801252`.

</details>

<details>

<summary>What countries are supported?</summary>

Any 2-letter storefront code is accepted by the API — it isn't restricted to a published list of supported countries.

</details>

<details>

<summary>How many reviews can I fetch in one call?</summary>

Up to 500 with `maxItems`, or up to 10 pages via `page` (page size isn't fixed by the spec). `page` and `maxItems` are mutually exclusive — use one or the other per request.

</details>

<details>

<summary>Can I filter reviews by star rating?</summary>

The API only supports sorting (`recent` or `helpful`), not server-side filtering by rating. Fetch the reviews you need and filter client-side on the `score` field.

</details>

<details>

<summary>What's the credit cost per request?</summary>

It's dynamic — every response includes `pricing.creditsCharged` with the exact amount for that call. There's no fixed published rate per endpoint or per result.

</details>

<details>

<summary>Can I export reviews to CSV?</summary>

The API returns JSON. Convert it to CSV with your language's tooling (e.g. Python's `csv` module) or load it directly into a BI tool.

</details>


# DuckDuckGo

The Pullbay DuckDuckGo API provides programmatic access to web, image, news, and video search results from DuckDuckGo. Integrate DuckDuckGo's search index into your applications, dashboards, and research pipelines without scraping or browser automation.

**Base URL:** `https://api.pullbay.com`

## Overview

The DuckDuckGo service lets you:

* Search the web for a term and get back ranked page results
* Search for images matching a term, including full-size and thumbnail URLs
* Search for news articles matching a term, optionally filtered by recency
* Search for videos matching a term, with duration, publisher, and view-count metadata

All four endpoints are read-only `GET` requests against public DuckDuckGo search results — there's no account or API key on DuckDuckGo's side required, just your Pullbay API key.

## Common Use Cases

* **Competitive Research** — Run the same query across web, news, and image results to see how a competitor, brand, or topic is currently represented in search.
* **Content Monitoring** — Poll the news or web endpoints on a schedule to catch new mentions of a brand, product, or keyword as they appear.
* **SEO / SERP Tracking** — Capture web search result rankings and snippets for a set of target terms over time to track visibility trends.
* **Brand Monitoring Across Media Types** — Combine the search, images, news, and videos endpoints to get a fuller picture of how a name or topic surfaces across different content formats.
* **Research and Due Diligence** — Pull web and news results for a company, person, or topic as a starting point for manual research, without manually running searches in a browser.

## Endpoints

| Endpoint                 | Description                         | Pagination |
| ------------------------ | ----------------------------------- | ---------- |
| `GET /duckduckgo/search` | Search the web via DuckDuckGo       | Offset     |
| `GET /duckduckgo/images` | Search images via DuckDuckGo        | Offset     |
| `GET /duckduckgo/news`   | Search news articles via DuckDuckGo | Offset     |
| `GET /duckduckgo/videos` | Search videos via DuckDuckGo        | Offset     |

***

### Search Web

Returns web search results for `term`.

**Request**

```bash
GET /duckduckgo/search
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter    | Type   | Required | Default | Description                                                                                            |
| ------------ | ------ | -------- | ------- | ------------------------------------------------------------------------------------------------------ |
| `term`       | string | Yes      | —       | Search term, 1–255 characters                                                                          |
| `region`     | string | Yes      | —       | DuckDuckGo region code. Closed list of 63 values — see [Region Codes](#region-codes)                   |
| `safeSearch` | string | Yes      | —       | One of `false`, `moderate`, `strict` — see [Safe Search Levels](#safe-search-levels)                   |
| `offset`     | number | No       | —       | Result offset, 0 or greater                                                                            |
| `time`       | string | No       | —       | Recency filter. One of `a`, `d`, `w`, `m`, `y` — see [Pagination and Recency](#pagination-and-recency) |

**Example**

```bash
curl -G "https://api.pullbay.com/duckduckgo/search" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d term="OpenAI" \
  -d region=us-en \
  -d safeSearch=moderate \
  -d offset=0
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "title": "OpenAI",
      "description": "We believe our research will eventually lead to artificial general intelligence, a system that can solve human-level problems.",
      "hostname": "openai.com",
      "icon": "https://external-content.duckduckgo.com/ip3/openai.com.ico",
      "url": "https://openai.com/"
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                             |
| ------------------------ | ------ | ------------------------------------------------------- |
| `data`                   | array  | Array of [Search Result objects](#search-result-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                        |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Search Images

Returns image search results for `term`.

**Request**

```bash
GET /duckduckgo/images
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter    | Type   | Required | Default | Description                                                                          |
| ------------ | ------ | -------- | ------- | ------------------------------------------------------------------------------------ |
| `term`       | string | Yes      | —       | Search term, 1–255 characters                                                        |
| `region`     | string | Yes      | —       | DuckDuckGo region code. Closed list of 63 values — see [Region Codes](#region-codes) |
| `safeSearch` | string | Yes      | —       | One of `false`, `moderate`, `strict` — see [Safe Search Levels](#safe-search-levels) |
| `offset`     | number | No       | —       | Result offset, 0 or greater                                                          |

> Note: unlike `search`, `news`, and `videos`, this endpoint has no `time` parameter.

**Example**

```bash
curl -G "https://api.pullbay.com/duckduckgo/images" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d term="wallpaper cat" \
  -d region=us-en \
  -d safeSearch=moderate \
  -d offset=0
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "title": "Wallpaper Cat Photos, Download The BEST Free Wallpaper Cat Stock Photos",
      "image": "https://images.pexels.com/photos/20331914/pexels-photo-20331914.jpeg",
      "thumbnail": "https://tse1.mm.bing.net/th/id/OIP.oOmFm8pcnPy1pVPS46kxfQHaE8?r=0&pid=Api",
      "image_token": "a8b8c30ef6de5362d93dd6f5848ec661ae916727695b7e944138db4a60c5f6ca",
      "thumbnail_token": "036492c962e7830bd485903527a6caf439793418572197fbf6a743a8255f594b",
      "width": 6000,
      "height": 4000,
      "source": "Bing",
      "url": "https://www.pexels.com/search/Wallpaper%20cat/"
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                             |
| ------------------------ | ------ | --------------------------------------- |
| `data`                   | array  | Array of [Image objects](#image-object) |
| `pricing.creditsCharged` | number | Credits charged for this request        |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Search News

Returns news articles for `term`.

**Request**

```bash
GET /duckduckgo/news
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter    | Type   | Required | Default | Description                                                                                            |
| ------------ | ------ | -------- | ------- | ------------------------------------------------------------------------------------------------------ |
| `term`       | string | Yes      | —       | Search term, 1–255 characters                                                                          |
| `region`     | string | Yes      | —       | DuckDuckGo region code. Closed list of 63 values — see [Region Codes](#region-codes)                   |
| `safeSearch` | string | Yes      | —       | One of `false`, `moderate`, `strict` — see [Safe Search Levels](#safe-search-levels)                   |
| `offset`     | number | No       | —       | Result offset, 0 or greater                                                                            |
| `time`       | string | No       | —       | Recency filter. One of `a`, `d`, `w`, `m`, `y` — see [Pagination and Recency](#pagination-and-recency) |

**Example**

```bash
curl -G "https://api.pullbay.com/duckduckgo/news" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d term="Marvell Technology" \
  -d region=us-en \
  -d safeSearch=moderate \
  -d time=w
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "title": "Why Marvell Technology Stock Just Dropped",
      "excerpt": "Why Marvell's mad at Poet. Seems somebody let the cat out of the bag too soon...",
      "url": "https://www.fool.com/investing/2026/04/27/why-marvell-technology-stock-just-dropped/",
      "image": "https://g.foolcdn.com/image/?url=https%3A%2F%2Fg.foolcdn.com%2Feditorial%2Fimages%2F867388%2F5-red-stock-market-arrows-pointing-down.jpg",
      "date": 1777303200,
      "relativeTime": "21 hours ago",
      "syndicate": "bing",
      "isOld": false
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                           |
| ------------------------ | ------ | ----------------------------------------------------- |
| `data`                   | array  | Array of [News Article objects](#news-article-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                      |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Search Videos

Returns video search results for `term`.

**Request**

```bash
GET /duckduckgo/videos
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter    | Type   | Required | Default | Description                                                                          |
| ------------ | ------ | -------- | ------- | ------------------------------------------------------------------------------------ |
| `term`       | string | Yes      | —       | Search term, 1–255 characters                                                        |
| `region`     | string | Yes      | —       | DuckDuckGo region code. Closed list of 63 values — see [Region Codes](#region-codes) |
| `safeSearch` | string | Yes      | —       | One of `false`, `moderate`, `strict` — see [Safe Search Levels](#safe-search-levels) |
| `offset`     | number | No       | —       | Result offset, 0 or greater                                                          |

> Note: unlike `search` and `news`, this endpoint has no `time` parameter.

**Example**

```bash
curl -G "https://api.pullbay.com/duckduckgo/videos" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d term="cooking skills" \
  -d region=us-en \
  -d safeSearch=moderate \
  -d offset=0
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "title": "Gordon Ramsay's Top Basic Cooking Skills | Ultimate Cookery Course FULL EPISODE",
      "description": "Gordon Ramsay demonstrates some basic cooking skills as well as some easy to do recipes.",
      "url": "https://www.youtube.com/watch?v=FTociictyyE",
      "image": "https://tse4.mm.bing.net/th/id/OVP.dTE0hC-DCsYuM6UsczwKpgHgFo?pid=Api",
      "duration": "23:03",
      "publishedOn": "YouTube",
      "published": "2019-11-07T17:00:05.0000000",
      "publisher": "Gordon Ramsay",
      "viewCount": 9252292
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                             |
| ------------------------ | ------ | --------------------------------------- |
| `data`                   | array  | Array of [Video objects](#video-object) |
| `pricing.creditsCharged` | number | Credits charged for this request        |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                  |
| ------ | ---------------------------------------- |
| `400`  | Bad request — missing/invalid parameters |
| `402`  | Insufficient credits                     |
| `404`  | No results found                         |

## Object Schema

### Search Result object

| Field         | Type   | Description                        |
| ------------- | ------ | ---------------------------------- |
| `title`       | string | Page title                         |
| `description` | string | Snippet/summary text from the page |
| `hostname`    | string | Domain the result was served from  |
| `icon`        | string | Favicon URL                        |
| `url`         | string | Link to the page                   |

### Image object

| Field              | Type    | Description                                    |
| ------------------ | ------- | ---------------------------------------------- |
| `title`            | string  | Image title                                    |
| `image`            | string  | Full-size image URL                            |
| `thumbnail`        | string  | Thumbnail image URL                            |
| `image_token`      | string  | Opaque token identifying the full-size image   |
| `thumbnail_token`  | string  | Opaque token identifying the thumbnail         |
| `width` / `height` | integer | Image dimensions in pixels                     |
| `source`           | string  | Originating image search backend (e.g. `Bing`) |
| `url`              | string  | Link to the page hosting the image             |

### News Article object

| Field          | Type    | Description                                                |
| -------------- | ------- | ---------------------------------------------------------- |
| `title`        | string  | Article headline                                           |
| `excerpt`      | string  | Short snippet of the article body                          |
| `url`          | string  | Link to the full article                                   |
| `image`        | string  | Associated image URL                                       |
| `date`         | integer | Publish timestamp, Unix seconds                            |
| `relativeTime` | string  | Human-readable relative publish time (e.g. `21 hours ago`) |
| `syndicate`    | string  | Syndication source (e.g. `bing`)                           |
| `isOld`        | boolean | Whether the article is flagged as stale/older content      |

### Video object

| Field         | Type               | Description                                       |
| ------------- | ------------------ | ------------------------------------------------- |
| `title`       | string             | Video title                                       |
| `description` | string             | Video description                                 |
| `url`         | string             | Link to the video                                 |
| `image`       | string             | Thumbnail/preview image URL                       |
| `duration`    | string             | Video duration, formatted `mm:ss` (or `hh:mm:ss`) |
| `publishedOn` | string             | Hosting platform (e.g. `YouTube`)                 |
| `published`   | string (date-time) | Publish timestamp                                 |
| `publisher`   | string             | Channel or publisher name                         |
| `viewCount`   | integer            | View count                                        |

## Region Codes

`region` is a closed enum, identical across all four endpoints. It is **not** a plain ISO country code — it's a DuckDuckGo-specific region identifier, generally a `<country>-<language>` pair, with a handful of exceptions (`wt-wt` for worldwide/no region, `ct-ca`, `xa-ar`). The API only accepts the 63 values below; any other value is invalid.

**Valid `region` values:**

`wt-wt`, `ar-es`, `au-en`, `at-de`, `be-fr`, `be-nl`, `br-pt`, `bg-bg`, `ca-en`, `ca-fr`, `ct-ca`, `cl-es`, `cn-zh`, `co-es`, `hr-hr`, `cz-cs`, `dk-da`, `ee-et`, `fi-fi`, `fr-fr`, `de-de`, `gr-el`, `hk-tzh`, `hu-hu`, `in-en`, `id-en`, `ie-en`, `il-en`, `it-it`, `jp-jp`, `kr-kr`, `lv-lv`, `lt-lt`, `my-en`, `mx-es`, `nl-nl`, `nz-en`, `no-no`, `pk-en`, `pe-es`, `ph-en`, `pl-pl`, `pt-pt`, `ro-ro`, `ru-ru`, `xa-ar`, `sg-en`, `sk-sk`, `sl-sl`, `za-en`, `es-ca`, `es-es`, `se-sv`, `ch-de`, `ch-fr`, `tw-tzh`, `th-en`, `tr-tr`, `us-en`, `us-es`, `ua-uk`, `uk-en`, `vn-en`

Use `wt-wt` when you don't want to bias results to a specific region. There is no documented default in the spec — `region` is required on every request, so you must pass one of the values above explicitly.

## Safe Search Levels

`safeSearch` is required on all four endpoints and takes one of three values:

* `false` — safe search off. Note that this is the literal boolean-looking token `false`, not a synonym like `off`; in the underlying spec it is written as the unquoted value `false` alongside the two quoted strings below.
* `moderate` — moderate filtering.
* `strict` — strict filtering.

There is no documented default — pass one of the three values explicitly on every request.

## Python Example: Paging Through Web Results with Offset

This example fetches web search results a batch at a time using `offset`, stopping once a batch comes back empty or the requested number of batches has been pulled. There is no `hasNextPage` flag or cursor on this endpoint, so the loop here treats an empty `data` array as the end of results:

```python
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.pullbay.com"

def get_search_results(term, region="us-en", safe_search="moderate", batch_size=10, max_batches=5):
    all_results = []
    offset = 0
    for _ in range(max_batches):
        resp = requests.get(
            f"{BASE_URL}/duckduckgo/search",
            headers={"Authorization": f"Bearer {API_KEY}"},
            params={
                "term": term,
                "region": region,
                "safeSearch": safe_search,
                "offset": offset,
            },
        )
        resp.raise_for_status()
        body = resp.json()
        batch = body["data"]
        if not batch:
            break
        all_results.extend(batch)
        offset += len(batch)
    return all_results

results = get_search_results("OpenAI")
print(f"Fetched {len(results)} results")
```

Each request is billed independently, with the charged amount reported in that response's `pricing.creditsCharged` — `offset` is a plain query parameter on this service, not part of Pullbay's `page`/`cursor` pagination envelope used elsewhere, so there's no `pagination` object to inspect for a "next page" flag.

## FAQ

<details>

<summary>What's the difference between <code>region</code> and a country code?</summary>

`region` is a DuckDuckGo-specific identifier, not a plain ISO country code. Most values are `<country>-<language>` pairs (e.g. `us-en`, `uk-en`, `de-de`), but a few don't follow that pattern (`wt-wt`, `ct-ca`, `xa-ar`). It's a closed list of exactly 63 values — see [Region Codes](#region-codes).

</details>

<details>

<summary>Why is <code>safeSearch=false</code> not a boolean?</summary>

`safeSearch` is typed as a string parameter, and one of its three accepted values happens to be the literal token `false` (as opposed to a quoted string like `"off"`). Send it as you would any other enum value for this parameter.

</details>

<details>

<summary>Which endpoints support the <code>time</code> filter?</summary>

Only `search` and `news`. `images` and `videos` do not accept a `time` parameter at all.

</details>

<details>

<summary>How do I get the next page of results?</summary>

Use `offset`. There's no `page`, `cursor`, or `maxItems` parameter on any of these four endpoints — increase `offset` by the number of results you've already consumed to fetch the next batch.

</details>

<details>

<summary>What's the credit cost per request?</summary>

It's dynamic — every response includes `pricing.creditsCharged` with the exact amount for that call. There's no fixed published rate per endpoint or per result.

</details>

<details>

<summary>Can I search without specifying a region?</summary>

No — `region` is a required parameter on all four endpoints. If you don't want to bias results to a particular country or language, pass `wt-wt` (worldwide).

</details>

## Next Steps

* [**API Integration Guide**](broken://pages/68e75ab1a73d266350bd68301be87ff97cad563e) — integrate the Pullbay API into your backend application.
* [**n8n Integration Guide**](broken://pages/4308164455b56bbc694cf74c9768ea689c2fe719) — set up automated workflows using n8n with no coding required.
* [**Services Overview**](broken://pages/0d55376cbcca70097a683a71a593d3077825dbf1) — explore all available Pullbay services.


# Google Maps

The Pullbay Google Maps API provides programmatic access to place search, place details, and place reviews from Google Maps. Integrate location data, business listings, and customer review content into your applications, dashboards, and analytics pipelines without scraping or manual lookups.

**Base URL:** `https://api.pullbay.com`

## Overview

The Google Maps service lets you:

* Search Google Maps for places by free-text query, optionally biased toward a geographic viewport
* Look up full details for a single place by its Google Maps feature ID (FID)
* Fetch reviews for a place using its Google Maps URL

All endpoints are read-only `GET` requests against public Google Maps data — you don't need a Google account or API key of your own.

## Common Use Cases

**Local SEO** — Pull a business's place details and review volume to track how it appears on Google Maps for a given category or area.

**Competitor Location Research** — Search a category in a target city or neighborhood to map out who's listed, where, and how they're rated.

**Review Monitoring for Multi-Location Businesses** — Fetch reviews per location on a schedule and route new ones into your own alerting or response workflow.

**Market Research** — Use Search Places across multiple queries or viewports to understand the density and distribution of a business category in a region.

**Due Diligence** — Look up a specific place by FID to confirm its current rating, review count, and contact details before including it in a report.

## Endpoints

| Endpoint                         | Description                                  | Pagination          |
| -------------------------------- | -------------------------------------------- | ------------------- |
| `GET /google-maps/place/search`  | Search Google Maps places by free-text query | Offset / `maxItems` |
| `GET /google-maps/place/{fid}`   | Get full details for a place by FID          | —                   |
| `GET /google-maps/place/reviews` | Get reviews for a place by Google Maps URL   | Cursor / `maxItems` |

***

### Search Places

Search Google Maps for places matching a free-text `query`. Optional geo filters (`latitude`, `longitude`, `zoom`) bias results toward a specific map viewport. Paginate with `offset`, or use `maxItems` for a one-shot bulk pull — the two are mutually exclusive.

**Request**

```bash
GET /google-maps/place/search
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter      | Type    | Required | Default | Description                                                                                               |
| -------------- | ------- | -------- | ------- | --------------------------------------------------------------------------------------------------------- |
| `query`        | string  | Yes      | —       | Free-text search term, 1–200 characters (e.g. `pizza near soho new york`)                                 |
| `countryCode`  | string  | No       | —       | Country code, 2–5 characters. Not restricted to a published list — biases results toward a country/region |
| `languageCode` | string  | No       | —       | Language code for localized text, 2–10 characters. Not restricted to a published list                     |
| `latitude`     | string  | No       | —       | Latitude to center the search viewport, as a numeric string (pattern `^-?\d+(\.\d+)?$`)                   |
| `longitude`    | string  | No       | —       | Longitude to center the search viewport, as a numeric string (pattern `^-?\d+(\.\d+)?$`)                  |
| `zoom`         | string  | No       | —       | Map zoom level to use alongside `latitude`/`longitude`, as a numeric string (pattern `^\d+(\.\d+)?$`)     |
| `offset`       | string  | No       | —       | Numeric offset string to paginate results (pattern `^\d+$`). Mutually exclusive with `maxItems`           |
| `maxItems`     | integer | No       | —       | One-shot bulk pull, 1–500 results. Mutually exclusive with `offset`                                       |

**Example**

```bash
curl -G "https://api.pullbay.com/google-maps/place/search" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d query="pizza near soho new york" \
  -d countryCode=us \
  -d offset=0
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "url": "https://www.google.com/maps/place/data=!3m1!4b1!4m2!3m1!1s0x89c25985f7ec1beb:0x3c39f655da433d9d",
      "title": "Prince Street Pizza",
      "description": null,
      "address": "27 Prince St, New York, NY 10012",
      "neighborhood": "Manhattan",
      "street": "27 Prince St",
      "district": "New York",
      "postalCode": "10012",
      "city": "New York",
      "country": "United States",
      "location": { "lat": 40.7230777, "lng": -73.9945444 },
      "website": "https://locations.princestreetpizza.com/new-york",
      "phone": "(212) 966-4100",
      "price": "$10–20",
      "totalScore": 4.4,
      "placeId": "ChIJ6xvs94VZwokRnT1D2lX2OTw",
      "fid": "0x89c25985f7ec1beb:0x3c39f655da433d9d",
      "reviewsCount": 9039,
      "categories": ["Pizza restaurant"],
      "openingHours": [],
      "additionalInfo": {}
    }
  ],
  "pagination": { "page": 1, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                                                              | Type             | Description                                                                                                           |
| ------------------------------------------------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------- |
| `data`                                                             | array            | Array of [Place search result objects](#place-search-result-object)                                                   |
| `pagination.offset`                                                | string, nullable | Offset-style pagination cursor for this endpoint — increment by the number of items received to request the next page |
| `pagination.page` / `pagination.hasNextPage` / `pagination.cursor` | nullable         | Not the pagination mechanism used by this endpoint — see Pagination Notes below                                       |
| `pricing.creditsCharged`                                           | number           | Credits charged for this request                                                                                      |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Get Place

Returns full details for a single Google Maps place identified by its FID. This is a single-record lookup — the response is not paginated.

**Request**

```bash
GET /google-maps/place/{fid}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter      | Type   | Required | Default | Description                                                                                                                                                                                 |
| -------------- | ------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fid`          | string | Yes      | —       | Path parameter. Google Maps feature ID, pattern `^0x[a-fA-F0-9]+:0x[a-fA-F0-9]+$` — two hex groups, each prefixed `0x`, separated by a colon (e.g. `0x47e671d877937b0f:0xb975fcfa192f84d4`) |
| `countryCode`  | string | No       | —       | Country code, 2–5 characters. Not restricted to a published list                                                                                                                            |
| `languageCode` | string | No       | —       | Language code for localized text, 2–10 characters. Not restricted to a published list                                                                                                       |

**Example**

```bash
curl -G "https://api.pullbay.com/google-maps/place/0x47e671d877937b0f:0xb975fcfa192f84d4" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d languageCode=en
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "url": "https://www.google.com/maps/preview/place/Louvre+Museum",
      "title": "Louvre Museum",
      "address": "Louvre Museum, 75001 Paris, France",
      "neighborhood": null,
      "street": null,
      "district": "Paris",
      "postalCode": "75001",
      "city": null,
      "location": { "lat": 40.7230777, "lng": -73.9945444 },
      "website": "https://www.louvre.fr/",
      "phone": "+33 1 40 20 53 17",
      "phoneUnformatted": "+33140205317",
      "plusCode": "V86Q+63 Paris, France",
      "price": null,
      "totalScore": 4.7,
      "placeId": "ChIJD3uTd9hx5kcR1IQvGfr8dbk",
      "cid": "-5082878453323168556",
      "fid": "0x47e671d877937b0f:0xb975fcfa192f84d4",
      "reviewsCount": 366153,
      "reviewsDistribution": {
        "oneStar": 5180,
        "twoStar": 3447,
        "threeStar": 13286,
        "fourStar": 52471,
        "fiveStar": 291769
      },
      "categories": ["Art museum", "Museum", "Tourist attraction"],
      "additionalInfo": {}
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                             |
| ------------------------ | ------ | ------------------------------------------------------- |
| `data`                   | array  | Array containing a single [Place object](#place-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                        |

There is no `pagination` object in this response — `getGoogleMapsPlace` returns one place per request, not a list.

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Get Place Reviews

Returns reviews for a place identified by its full Google Maps URL (`placeUrl`) — not its FID. Paginate with `cursor`, or use `maxItems` for a one-shot bulk pull — the two are mutually exclusive.

**Request**

```bash
GET /google-maps/place/reviews
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Default | Description                                                                                                             |
| ---------- | ------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------- |
| `placeUrl` | string  | Yes      | —       | Full Google Maps place URL, 10–1000 characters. This is a different identifier than `fid` — see Finding Place IDs below |
| `maxItems` | integer | No       | —       | One-shot bulk pull, 1–500 results. Mutually exclusive with `cursor`                                                     |
| `cursor`   | string  | No       | —       | Opaque pagination cursor returned in the previous response's `pagination.cursor`. Mutually exclusive with `maxItems`    |

**Example**

```bash
curl -G "https://api.pullbay.com/google-maps/place/reviews" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d placeUrl="https://www.google.com/maps/place/data=!3m1!4b1!4m2!3m1!1s0x89c25985f7ec1beb:0x3c39f655da433d9d" \
  -d maxItems=20
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "Ci9DQUlRQUNvZENodHljRjlvT25OdmExZHZVSEJPYjJkRGIxVXpiRFIzYXpWUmFFRRAB",
      "link": "https://www.google.com/maps/reviews/data=!4m8!14m7!1m6!2m5!1sCi9DQUlRQUNvZENodHljRjlvT25OdmExZHZVSEJPYjJkRGIxVXpiRFIzYXpWUmFFRRAB",
      "text": "Loved the experience",
      "stars": 5,
      "date": 1777380220000,
      "dateIso": "2026-04-28T12:43:40.000Z",
      "likes": null,
      "photos": [],
      "reviewer": {
        "name": "Jane Doe",
        "photo": "https://example.com/reviewer-photo.jpg",
        "url": "https://www.google.com/maps/contrib/000000000000000000000?hl=en-US",
        "totalReviews": 2,
        "totalPhotos": 7
      },
      "ownersResponse": {},
      "details": {
        "Visited on": "Weekend",
        "Wait time": "10–30 min",
        "Reservation recommended": true
      }
    }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                                                              | Type             | Description                                                                                           |
| ------------------------------------------------------------------ | ---------------- | ----------------------------------------------------------------------------------------------------- |
| `data`                                                             | array            | Array of [Place review objects](#place-review-object)                                                 |
| `pagination.cursor`                                                | string, nullable | Cursor-style pagination token for this endpoint — pass it as `cursor` on the next request to continue |
| `pagination.page` / `pagination.hasNextPage` / `pagination.offset` | nullable         | Not the pagination mechanism used by this endpoint                                                    |
| `pricing.creditsCharged`                                           | number           | Credits charged for this request                                                                      |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                  |
| ------ | ---------------------------------------- |
| `400`  | Bad request — missing/invalid parameters |
| `402`  | Insufficient credits                     |
| `404`  | Place or review set not found            |

All three endpoints can return `400`, `402`, and `404`.

## Object Schema

### Place search result object

| Field            | Type             | Description                                                                                            |
| ---------------- | ---------------- | ------------------------------------------------------------------------------------------------------ |
| `url`            | string           | Google Maps URL for the place                                                                          |
| `title`          | string           | Place name                                                                                             |
| `description`    | string, nullable | Short description, if available                                                                        |
| `address`        | string           | Full formatted address                                                                                 |
| `neighborhood`   | string, nullable | Neighborhood name                                                                                      |
| `street`         | string, nullable | Street address                                                                                         |
| `district`       | string, nullable | District or city-level area                                                                            |
| `postalCode`     | string, nullable | Postal code                                                                                            |
| `city`           | string, nullable | City name                                                                                              |
| `country`        | string, nullable | Country name                                                                                           |
| `location`       | object           | Latitude/longitude pair — see Location object                                                          |
| `website`        | string, nullable | Place's website URL                                                                                    |
| `phone`          | string, nullable | Formatted phone number                                                                                 |
| `price`          | string, nullable | Price range indicator (e.g. `$10–20`)                                                                  |
| `totalScore`     | number           | Average rating                                                                                         |
| `placeId`        | string           | Google Place ID                                                                                        |
| `fid`            | string           | Google Maps feature ID, of the form `0x...:0x...` — use this to fetch full place details via Get Place |
| `reviewsCount`   | integer          | Total number of reviews                                                                                |
| `categories`     | array            | Place category labels                                                                                  |
| `openingHours`   | array            | Opening hours per weekday                                                                              |
| `additionalInfo` | object           | Grouped feature flags (e.g. Service options, Accessibility, Offerings)                                 |

### Location object

| Field | Type   | Description |
| ----- | ------ | ----------- |
| `lat` | number | Latitude    |
| `lng` | number | Longitude   |

### Place object

| Field                 | Type             | Description                                                               |
| --------------------- | ---------------- | ------------------------------------------------------------------------- |
| `url`                 | string           | Google Maps URL for the place                                             |
| `title`               | string           | Place name                                                                |
| `address`             | string           | Full formatted address                                                    |
| `neighborhood`        | string, nullable | Neighborhood name                                                         |
| `street`              | string, nullable | Street address                                                            |
| `district`            | string, nullable | District or city-level area                                               |
| `postalCode`          | string, nullable | Postal code                                                               |
| `city`                | string, nullable | City name                                                                 |
| `location`            | object           | Latitude/longitude pair — see Location object                             |
| `website`             | string, nullable | Place's website URL                                                       |
| `phone`               | string, nullable | Formatted phone number                                                    |
| `phoneUnformatted`    | string, nullable | Phone number without formatting                                           |
| `plusCode`            | string, nullable | Google Plus Code                                                          |
| `price`               | string, nullable | Price range indicator                                                     |
| `totalScore`          | number           | Average rating                                                            |
| `placeId`             | string           | Google Place ID                                                           |
| `cid`                 | string           | Google Maps client ID                                                     |
| `fid`                 | string           | Google Maps feature ID, of the form `0x...:0x...`                         |
| `reviewsCount`        | integer          | Total number of reviews                                                   |
| `reviewsDistribution` | object           | Review count broken down by star rating — see Reviews distribution object |
| `categories`          | array            | Place category labels                                                     |
| `additionalInfo`      | object           | Grouped feature flags (e.g. Service options, Accessibility, Amenities)    |

### Reviews distribution object

| Field       | Type    | Description             |
| ----------- | ------- | ----------------------- |
| `oneStar`   | integer | Count of 1-star reviews |
| `twoStar`   | integer | Count of 2-star reviews |
| `threeStar` | integer | Count of 3-star reviews |
| `fourStar`  | integer | Count of 4-star reviews |
| `fiveStar`  | integer | Count of 5-star reviews |

### Place review object

| Field            | Type               | Description                                                   |
| ---------------- | ------------------ | ------------------------------------------------------------- |
| `id`             | string             | Review ID                                                     |
| `link`           | string             | Permalink to the review                                       |
| `text`           | string             | Review body                                                   |
| `stars`          | integer            | Rating, 1–5                                                   |
| `date`           | integer            | Posted timestamp, Unix milliseconds                           |
| `dateIso`        | string (date-time) | Posted timestamp, ISO 8601                                    |
| `likes`          | integer, nullable  | Number of likes on the review                                 |
| `photos`         | array              | Photo URLs attached to the review                             |
| `reviewer`       | object             | Reviewer who left the review — see Reviewer object            |
| `ownersResponse` | object             | Owner's response to the review (empty object when none)       |
| `details`        | object             | Free-form key/value metadata (e.g. `Visited on`, `Wait time`) |

### Reviewer object

| Field          | Type    | Description                                |
| -------------- | ------- | ------------------------------------------ |
| `name`         | string  | Reviewer's display name                    |
| `photo`        | string  | Reviewer's profile photo URL               |
| `url`          | string  | Link to the reviewer's Google Maps profile |
| `totalReviews` | integer | Total reviews the reviewer has written     |
| `totalPhotos`  | integer | Total photos the reviewer has uploaded     |

## Finding Place IDs

Search Places, Get Place, and Get Place Reviews each expect a different identifier — they are **not** interchangeable:

* **FID**: the feature ID returned as `fid` on both [Place search result objects](#place-search-result-object) and [Place objects](#place-object), of the form `0x47e671d877937b0f:0xb975fcfa192f84d4` (two `0x`-prefixed hex groups separated by a colon). Run Search Places first to discover a place's `fid`, then pass it as the path parameter to Get Place (`/google-maps/place/{fid}`) for full details.
* **Place URL**: the full `google.com/maps/place/...` URL for a place, returned as `url` on both search and detail responses. Pass this — not the `fid` — as `placeUrl` to Get Place Reviews (`/google-maps/place/reviews`). An `fid` cannot be substituted for `placeUrl`; the reviews endpoint requires the full URL string.

In short: get the `fid` from a search result to look up place details, but get the `url` from that same search result to look up reviews.

## Python Example: Searching, Then Fetching Reviews

This example searches for places matching a query, then uses the `url` from the first result to fetch its reviews — illustrating why `fid` and `placeUrl` are not interchangeable:

```python
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.pullbay.com"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

def search_places(query, country_code="us", max_items=10):
    resp = requests.get(
        f"{BASE_URL}/google-maps/place/search",
        headers=HEADERS,
        params={"query": query, "countryCode": country_code, "maxItems": max_items},
    )
    resp.raise_for_status()
    return resp.json()["data"]

def get_reviews_for_place(place_url, max_items=20):
    resp = requests.get(
        f"{BASE_URL}/google-maps/place/reviews",
        headers=HEADERS,
        params={"placeUrl": place_url, "maxItems": max_items},
    )
    resp.raise_for_status()
    return resp.json()["data"]

places = search_places("pizza near soho new york")
first_place = places[0]

# Note: first_place["fid"] would be used with /google-maps/place/{fid} to get
# full place details. Reviews require the full URL instead:
reviews = get_reviews_for_place(first_place["url"])
print(f"Fetched {len(reviews)} reviews for {first_place['title']}")
```

Each request is billed independently, with the charged amount reported in that response's `pricing.creditsCharged` — there's no separate "fetch everything in one call" endpoint.

## FAQ

<details>

<summary>What's the difference between <code>fid</code> and <code>placeUrl</code>?</summary>

They're different identifiers for different endpoints. `fid` is Google's feature-ID format (`0x...:0x...`) used to fetch full place details from Get Place. `placeUrl` is the full `google.com/maps/place/...` URL used to fetch reviews from Get Place Reviews. Both are returned alongside each other on search and detail responses, but one cannot be substituted for the other.

</details>

<details>

<summary>How do I paginate through search results?</summary>

Use the `offset` parameter (a numeric string) and increment it by the number of items received in each response to request the next page. Alternatively, use `maxItems` for a one-shot bulk pull instead of paging — `offset` and `maxItems` are mutually exclusive.

</details>

<details>

<summary>How do I paginate through reviews?</summary>

Use the `cursor` returned in the previous response's `pagination.cursor` field to request the next page. Alternatively, use `maxItems` for a one-shot bulk pull — `cursor` and `maxItems` are mutually exclusive.

</details>

<details>

<summary>Why doesn't Get Place have pagination?</summary>

It's a single-record lookup — you pass one `fid` and get back one place. There's no list to page through, so the response has no `pagination` object.

</details>

<details>

<summary>Are <code>countryCode</code> and <code>languageCode</code> restricted to specific values?</summary>

No. Both are length-constrained strings (`countryCode` 2–5 characters, `languageCode` 2–10 characters) but are not validated against a published enum or fixed list in the API spec.

</details>

<details>

<summary>What's the credit cost per request?</summary>

It's dynamic — every response includes `pricing.creditsCharged` with the exact amount for that call. There's no fixed published rate per endpoint or per result.

</details>

## Next Steps

* [**API Integration Guide**](broken://pages/68e75ab1a73d266350bd68301be87ff97cad563e) — integrate the Pullbay API into your backend application.
* [**n8n Integration Guide**](broken://pages/4308164455b56bbc694cf74c9768ea689c2fe719) — set up automated workflows using n8n with no coding required.
* [**Services Overview**](broken://pages/0d55376cbcca70097a683a71a593d3077825dbf1) — explore all available Pullbay services.


# Google News

The Pullbay Google News API provides programmatic access to Google News articles — search by keyword or browse by category. Retrieve article titles, source names, publication timestamps, images, and original article URLs without scraping.

**Base URL:** `https://api.pullbay.com`

## Overview

The Google News service lets you:

* Search Google News by keyword, with optional language/region filtering
* Browse the top articles in a category (Business, Technology, Sports, and more)
* Optionally resolve Google's redirect URLs to direct article URLs

All endpoints are read-only `GET` requests. No Google credentials are required — only your Pullbay API key.

## Common Use Cases

**News Monitoring** — Search a keyword on a schedule to track breaking news, brand mentions, or topic coverage over time.

**Competitive Intelligence** — Monitor news coverage for competitors, products, or industry keywords and surface new articles as they appear.

**Content Aggregation** — Pull top articles by category to populate a news feed, newsletter, or dashboard with fresh content.

**Sentiment & Trend Analysis** — Feed article titles and sources into an NLP pipeline to track how media sentiment around a topic shifts over time.

**Research & Alerting** — Trigger alerts when new articles appear for specific queries (e.g. a company name, a product launch, a regulatory topic).

## Endpoints

| Endpoint                    | Description                     | Pagination             |
| --------------------------- | ------------------------------- | ---------------------- |
| `GET /google-news/search`   | Search articles by keyword      | `maxItems` (bulk only) |
| `GET /google-news/category` | Browse top articles by category | — (full result set)    |

## Search News

Search Google News articles by keyword, with optional language/region and URL resolution.

**Request**

```bash
GET /google-news/search
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter      | Type    | Required | Default | Description                                                                                                                                    |
| -------------- | ------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `query`        | string  | Yes      | —       | Search keyword, 1–500 characters                                                                                                               |
| `languageCode` | string  | No       | `US:en` | Language and region code (see [Language Codes](#language-codes)). Defaults to `US:en` (English, United States)                                 |
| `resolveUrl`   | string  | No       | `no`    | `yes` to resolve Google redirect URLs to direct article URLs. Adds a `resolvedUrl` field to each article. `no` returns the raw Google News URL |
| `maxItems`     | integer | No       | 100     | Maximum articles to return, 1–100                                                                                                              |

**Example**

```bash
curl -G "https://api.pullbay.com/google-news/search" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d query="artificial intelligence" \
  -d languageCode="US:en" \
  -d resolveUrl="no" \
  -d maxItems=20
```

**Python**

```python
import requests

response = requests.get(
    "https://api.pullbay.com/google-news/search",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    params={
        "query": "artificial intelligence",
        "languageCode": "US:en",
        "resolveUrl": "no",
        "maxItems": 20,
    },
)

body = response.json()
print(f"requestId: {body['requestId']}, credits: {body['pricing']['creditsCharged']}")
for article in body["data"]:
    print(f"{article['publishedAt']}  {article['source']}  {article['title']}")
```

**Response**

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "title": "The next phase of the Microsoft-OpenAI partnership",
      "articleUrl": "https://news.google.com/read/CBMimwFBVV95cUxNakYwXzBZNG85QlRVd01tNzEwaEM3djQxWnMya0FN",
      "publishedAt": "2026-04-27T13:07:55Z",
      "imageUrl": "/api/attachments/CC8iK0NnNXdhR2hQYWxjeGIxSmpNMEpOVFJDZk...",
      "source": "The Official Microsoft Blog",
      "sourceIconUrl": "https://encrypted-tbn0.gstatic.com/faviconV2?url=https://blogs.microsoft.com",
      "author": "By Brendan Morrow"
    }
  ],
  "pagination": {
    "page": null,
    "hasNextPage": null,
    "cursor": null,
    "offset": null
  },
  "pricing": { "creditsCharged": 20 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                      |
| ------------------------ | ------ | ---------------------------------------------------------------- |
| `data`                   | array  | Array of [GoogleNewsArticle objects](#googlenewsarticle-object)  |
| `pagination`             | object | All fields are `null` — this endpoint does not paginate          |
| `pricing.creditsCharged` | number | Credits charged for this request (1 credit per article returned) |

**Credit cost:** 1 credit per article returned. A request returning 20 articles costs 20 credits.

## News by Category

Returns the current top articles for a Google News category. Returns up to 300 articles in a single call — no pagination.

**Request**

```bash
GET /google-news/category
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter      | Type   | Required | Default | Description                                                                                        |
| -------------- | ------ | -------- | ------- | -------------------------------------------------------------------------------------------------- |
| `category`     | string | Yes      | —       | One of: `business`, `entertainment`, `health`, `latest`, `science`, `sport`, `technology`, `world` |
| `languageCode` | string | No       | `US:en` | Language and region code (see [Language Codes](#language-codes))                                   |

**Example**

```bash
curl -G "https://api.pullbay.com/google-news/category" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d category="technology" \
  -d languageCode="US:en"
```

**Python**

```python
import requests

response = requests.get(
    "https://api.pullbay.com/google-news/category",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    params={
        "category": "technology",
        "languageCode": "US:en",
    },
)

body = response.json()
print(f"requestId: {body['requestId']}, credits: {body['pricing']['creditsCharged']}")
for article in body["data"]:
    print(f"{article['publishedAt']}  {article['source']}  {article['title']}")
```

**Response**

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "title": "Apple announces new M4 MacBook lineup",
      "articleUrl": "https://news.google.com/read/CBMiaEFVX3lxTE9xUFpDVzNLWWx4Ml9MT0N5NUFtV1...",
      "publishedAt": "2026-04-28T09:15:00Z",
      "imageUrl": "/api/attachments/CC8iK0NnNXdhR2hQYWxjeGIxSmpNMEpOVFJD...",
      "source": "TechCrunch",
      "sourceIconUrl": "https://encrypted-tbn0.gstatic.com/faviconV2?url=https://techcrunch.com",
      "author": null
    }
  ],
  "pagination": {
    "page": null,
    "hasNextPage": null,
    "cursor": null,
    "offset": null
  },
  "pricing": { "creditsCharged": 85 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                                   |
| ------------------------ | ------ | ----------------------------------------------------------------------------- |
| `data`                   | array  | Array of [GoogleNewsArticle objects](#googlenewsarticle-object)               |
| `pagination`             | object | All fields are `null` — this endpoint returns the full result set in one call |
| `pricing.creditsCharged` | number | Credits charged for this request (1 credit per article returned)              |

**Credit cost:** 1 credit per article returned. Up to 300 articles per category.

## Errors

All non-2xx responses use the same envelope:

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 400,
  "message": "Validation failed",
  "success": false,
  "error": { "code": "VALIDATION_ERROR" }
}
```

| Status | Code                          | Cause                                                                    |
| ------ | ----------------------------- | ------------------------------------------------------------------------ |
| `400`  | `VALIDATION_ERROR`            | Missing or invalid parameter (e.g. unknown `category`, `query` too long) |
| `401`  | `AUTHENTICATION_UNAUTHORIZED` | Missing or invalid API key                                               |
| `402`  | `TEAM_INSUFFICIENT_CREDITS`   | Credit balance is 0                                                      |
| `429`  | `RATE_LIMIT_EXCEEDED`         | Request rate exceeded for your plan                                      |
| `5xx`  | —                             | Server-side error — retry with exponential backoff                       |

Failed requests do not consume credits.

## Object Schema

### GoogleNewsArticle object

| Field           | Type              | Description                                                                                                |
| --------------- | ----------------- | ---------------------------------------------------------------------------------------------------------- |
| `title`         | string            | Article headline                                                                                           |
| `articleUrl`    | string            | Google News redirect URL for the article. Resolves to the original publisher URL when `resolveUrl=yes`     |
| `publishedAt`   | string (ISO 8601) | Publication timestamp, e.g. `2026-04-27T13:07:55Z`                                                         |
| `imageUrl`      | string, nullable  | Thumbnail image path or URL. May be a relative path — prepend `https://news.google.com` if needed          |
| `source`        | string            | Publisher name, e.g. `TechCrunch`                                                                          |
| `sourceIconUrl` | string            | URL of the publisher's favicon                                                                             |
| `author`        | string, nullable  | Author byline if present, e.g. `By Jane Smith`. `null` if not available                                    |
| `resolvedUrl`   | string, nullable  | Direct link to the original article — only present when `resolveUrl=yes` was passed on the Search endpoint |

## Language Codes

Both endpoints accept a `languageCode` parameter in `COUNTRY:language` format. The default is `US:en`. There are 84 supported codes covering languages including English, Spanish, French, German, Portuguese, Arabic, Chinese, Japanese, Hindi, and more.

**Examples:**

| Code         | Region                            |
| ------------ | --------------------------------- |
| `US:en`      | English (United States) — default |
| `GB:en`      | English (United Kingdom)          |
| `DE:de`      | German (Germany)                  |
| `FR:fr`      | French (France)                   |
| `ES:es`      | Spanish (Spain)                   |
| `MX:es`      | Spanish (Mexico)                  |
| `BR:pt-419`  | Portuguese (Brazil)               |
| `JP:ja`      | Japanese (Japan)                  |
| `CN:zh-Hans` | Chinese Simplified (China)        |
| `IN:en`      | English (India)                   |
| `AU:en`      | English (Australia)               |
| `CA:en`      | English (Canada)                  |
| `AR:ar`      | Arabic (Saudi Arabia)             |

Pass the exact code string as the `languageCode` parameter value.

## URL Resolution

By default, `articleUrl` is a Google News redirect link (`https://news.google.com/read/...`). These links are valid and open in a browser, but they require a redirect to reach the original article.

Pass `resolveUrl=yes` on the Search endpoint to have Pullbay follow the redirect and return the direct publisher URL in a `resolvedUrl` field alongside the original `articleUrl`:

```json
{
  "title": "The next phase of the Microsoft-OpenAI partnership",
  "articleUrl": "https://news.google.com/read/CBMimwFBVV95cUxNakYwXzBZNG85...",
  "resolvedUrl": "https://blogs.microsoft.com/blog/2026/04/27/microsoft-openai-partnership/",
  "publishedAt": "2026-04-27T13:07:55Z",
  "source": "The Official Microsoft Blog"
}
```

`resolveUrl` is only available on the Search endpoint. The Category endpoint always returns raw Google News redirect URLs.

## Pagination

Neither endpoint supports `page`-by-page pagination.

* **Search** accepts `maxItems` (1–100) to control how many articles are returned in one call. There is no cursor or page mechanism — you receive up to 100 articles per request.
* **Category** always returns the full set of top articles for that category (up to 300) in a single call. There are no pagination parameters.

## Python Examples

### Keyword monitoring loop

Poll a keyword every hour and print new articles since the last run:

```python
import requests
from datetime import datetime, timezone, timedelta

API_KEY = "YOUR_API_KEY"

def fetch_recent_news(query, since_minutes=60):
    body = requests.get(
        "https://api.pullbay.com/google-news/search",
        headers={"Authorization": f"Bearer {API_KEY}"},
        params={"query": query, "maxItems": 100},
    ).json()

    cutoff = datetime.now(timezone.utc) - timedelta(minutes=since_minutes)
    recent = [
        a for a in body["data"]
        if datetime.fromisoformat(a["publishedAt"]) > cutoff
    ]
    print(f"requestId: {body['requestId']}, credits: {body['pricing']['creditsCharged']}")
    print(f"New articles in last {since_minutes} min: {len(recent)}")
    for a in recent:
        print(f"  {a['source']}: {a['title']}")
    return recent

fetch_recent_news("OpenAI")
```

### Fetch top technology news

```python
import requests

body = requests.get(
        "https://api.pullbay.com/google-news/category",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    params={"category": "technology", "languageCode": "US:en"},
).json()

print(f"requestId: {body['requestId']}, credits: {body['pricing']['creditsCharged']}")
for article in body["data"][:10]:
    print(f"{article['publishedAt']}  [{article['source']}]  {article['title']}")
```

### Resolve article URLs

```python
import requests

body = requests.get(
    "https://api.pullbay.com/google-news/search",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    params={"query": "climate change", "resolveUrl": "yes", "maxItems": 10},
).json()

for article in body["data"]:
    url = article.get("resolvedUrl") or article["articleUrl"]
    print(f"{article['title']}\n  {url}\n")
```

## FAQ

<details>

<summary>How often is the data refreshed?</summary>

Articles are fetched from Google News in real time on each request. Results reflect what Google News is currently surfacing for the query or category at the moment of the call. There is no cached snapshot — each request hits Google News live.

</details>

<details>

<summary>What is the maximum number of articles I can get?</summary>

Search returns up to 100 articles per call (`maxItems` 1–100). Category returns up to 300 articles per call. Neither endpoint supports additional pagination beyond these limits.

</details>

<details>

<summary>Why does <code>articleUrl</code> point to <code>news.google.com</code> instead of the publisher?</summary>

Google News wraps all article links in a redirect URL. These links open correctly in a browser. If you need the direct publisher URL for scraping, archiving, or deduplication, pass `resolveUrl=yes` on the Search endpoint — Pullbay will follow the redirect and return the final URL in `resolvedUrl`.

</details>

<details>

<summary>Can I filter by date range?</summary>

There is no server-side date filter. Fetch articles and filter by `publishedAt` in your own code:

```python
from datetime import datetime, timezone, timedelta

cutoff = datetime.now(timezone.utc) - timedelta(hours=24)
recent = [a for a in body["data"] if datetime.fromisoformat(a["publishedAt"]) > cutoff]
```

</details>

<details>

<summary>Does <code>resolveUrl=yes</code> cost extra credits?</summary>

No. URL resolution is included in the standard per-article credit cost. You are charged 1 credit per article returned regardless of whether `resolveUrl` is `yes` or `no`.

</details>

<details>

<summary>What language codes are supported?</summary>

84 codes are supported in `COUNTRY:language` format. The default is `US:en`. Examples include `GB:en`, `DE:de`, `FR:fr`, `JP:ja`, `BR:pt-419`, `CN:zh-Hans`. See [Language Codes](#language-codes) for a full sample list.

</details>


# Google Play Store

The Pullbay Google Play API provides programmatic access to Android app metadata, search, similar-app recommendations, reviews, permissions, and Data Safety declarations from the Google Play Store. Integrate Google Play data into your applications, dashboards, and analytics pipelines without scraping or manual lookups.

**Base URL:** `https://api.pullbay.com`

## Overview

The Google Play service lets you:

* Look up a single app by its package name (e.g. `com.instagram.android`)
* Search the Google Play Store by keyword
* Find apps similar to a given app
* Fetch reviews for an app, sorted by recency, rating, or helpfulness
* List the Android permissions an app requests
* Retrieve the developer-declared Data Safety section (data collected, data shared, security practices)

All endpoints are read-only `GET` requests against public Google Play data — you don't need to own or register an app of your own.

## Common Use Cases

**Competitor Analysis** — Pull a competitor's app metadata and reviews to track positioning, install-count tier, and rating trends over time.

**Sentiment Monitoring** — Collect reviews on a schedule and feed the `text` field into your own sentiment-analysis or alerting pipeline.

**Market Research** — Use Search Apps and Get Similar Apps together to map out a category and see how apps cluster around features and audiences.

**Privacy and Compliance Review** — Use Get Permissions and Get Data Safety to audit what an app collects, shares, and declares before integrating with it or evaluating it as a competitor.

**Product Development** — Mine review text for recurring feature requests and complaints to help prioritize a roadmap.

## Endpoints

| Endpoint                       | Description                            | Pagination            |
| ------------------------------ | -------------------------------------- | --------------------- |
| `GET /google-play/apps`        | Get a single app by App ID             | —                     |
| `GET /google-play/search`      | Search apps by term                    | —                     |
| `GET /google-play/similar`     | Get apps similar to a given app        | —                     |
| `GET /google-play/reviews`     | Get reviews for an app                 | `cursor` / `maxItems` |
| `GET /google-play/permissions` | Get permissions requested by an app    | —                     |
| `GET /google-play/data-safety` | Get the Data Safety section for an app | —                     |

***

### Get App

Returns full details for a single app on the Google Play Store, identified by its package name.

**Request**

```bash
GET /google-play/apps
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Default | Description                                                                                  |
| --------- | ------ | -------- | ------- | -------------------------------------------------------------------------------------------- |
| `appId`   | string | Yes      | —       | Package name, max 255 characters, letters/digits/`.`/`_` only (e.g. `com.instagram.android`) |
| `country` | string | No       | —       | Exactly 2 characters. Not restricted to a fixed list — any 2-letter code is accepted.        |
| `lang`    | string | No       | —       | Language code for localized text, pattern `^[a-z]{2,3}(-[a-z]{2,3})?$` (e.g. `en`, `en-us`)  |

**Example**

```bash
curl -G "https://api.pullbay.com/google-play/apps" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d appId=com.instagram.android \
  -d country=us
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "title": "Instagram",
      "appId": "com.instagram.android",
      "url": "/api/apps/com.instagram.android",
      "icon": "https://play-lh.googleusercontent.com/VRMWkE5p3CkWhJs6nv-9ZsLAs1QOg5ob1_3qg-rckwYW7yp1fMrYZqnEFpk0IoVP4LM",
      "headerImage": "https://play-lh.googleusercontent.com/CgUPurtUp8SB709wsQcKLMv6JdIh1em0ka_58V69AorznLv42TV39etS2rt28wv9Rz8",
      "summary": "Create &amp; share photos, stories, &amp; reels with friends you love",
      "description": "Little moments lead to big friendships. Share yours on Instagram.",
      "descriptionHTML": "Little moments lead to big friendships. Share yours on Instagram. <br>—From Meta",
      "score": 4.0182447,
      "scoreText": "4.0",
      "ratings": 167461256,
      "reviews": 4568306,
      "histogram": { "1": 30279668, "2": 5233417, "3": 7039797, "4": 13507336, "5": 111401011 },
      "free": true,
      "price": 0,
      "priceText": "Free",
      "currency": "USD",
      "offersIAP": true,
      "adSupported": true,
      "installs": "5,000,000,000+",
      "minInstalls": 5000000000,
      "maxInstalls": 7427343782,
      "androidVersion": "VARY",
      "androidVersionText": "Varies with device",
      "updated": 1777328205000,
      "genre": "Social",
      "genreId": "SOCIAL",
      "developer": "Instagram",
      "developerId": "Instagram",
      "developerEmail": "instagram-android@meta.com",
      "developerWebsite": "http://help.instagram.com/",
      "developerInternalID": "Instagram",
      "privacyPolicy": "http://instagram.com/legal/privacy/",
      "playstoreUrl": "https://play.google.com/store/apps/details?id=com.instagram.android&hl=en&gl=us",
      "screenshots": ["https://play-lh.googleusercontent.com/hZ2Ob6d0MqLse2lIdNOGgl93H5y82uhX4hC6mWDUpdwK8LjXAunweDQZ6Hocy0riv4KX0lTIzKJz6lhRDV_BiHg"]
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                         |
| ------------------------ | ------ | ------------------------------------------------------------------- |
| `data`                   | array  | Array containing a single [Application object](#application-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                                    |

**Credit cost:** charged dynamically per request; the exact amount is always returned in `pricing.creditsCharged`. There is no published flat rate — read the figure from the response.

***

### Search Apps

Search the Google Play Store for apps matching a term. Returns lightweight summaries.

**Request**

```bash
GET /google-play/search
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Default | Description                                                            |
| --------- | ------ | -------- | ------- | ---------------------------------------------------------------------- |
| `search`  | string | Yes      | —       | Search term, 1–255 characters                                          |
| `price`   | string | No       | —       | Filter by pricing: `all`, `free`, or `paid`                            |
| `country` | string | No       | —       | Exactly 2 characters. Not restricted to a fixed list.                  |
| `lang`    | string | No       | —       | Language code for localized text, pattern `^[a-z]{2,3}(-[a-z]{2,3})?$` |

**Example**

```bash
curl -G "https://api.pullbay.com/google-play/search" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d search="photo editor" \
  -d price=free \
  -d country=us
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "title": "Instagram",
      "appId": "com.instagram.android",
      "url": "/api/apps/com.instagram.android",
      "icon": "https://play-lh.googleusercontent.com/VRMWkE5p3CkWhJs6nv-9ZsLAs1QOg5ob1_3qg-rckwYW7yp1fMrYZqnEFpk0IoVP4LM",
      "summary": "Create &amp; share photos, stories, &amp; reels with friends you love",
      "score": 4.0182447,
      "scoreText": "4.0",
      "free": true,
      "price": 0,
      "currency": "USD",
      "playstoreUrl": "https://play.google.com/store/apps/details?id=com.instagram.android",
      "developer": "Instagram",
      "developerId": "Instagram"
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                         |
| ------------------------ | ------ | ------------------------------------------------------------------- |
| `data`                   | array  | Array of [Application summary objects](#application-summary-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                                    |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No flat per-result rate is published — read the figure from each response.

***

### Get Similar Apps

Returns apps Google Play surfaces as similar to the supplied app.

**Request**

```bash
GET /google-play/similar
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Default | Description                                                   |
| --------- | ------ | -------- | ------- | ------------------------------------------------------------- |
| `appId`   | string | Yes      | —       | Package name, max 255 characters, letters/digits/`.`/`_` only |

**Example**

```bash
curl -G "https://api.pullbay.com/google-play/similar" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d appId=com.instagram.android
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "title": "Instagram",
      "appId": "com.instagram.android",
      "url": "/api/apps/com.instagram.android",
      "icon": "https://play-lh.googleusercontent.com/VRMWkE5p3CkWhJs6nv-9ZsLAs1QOg5ob1_3qg-rckwYW7yp1fMrYZqnEFpk0IoVP4LM",
      "summary": "Create &amp; share photos, stories, &amp; reels with friends you love",
      "score": 4.0182447,
      "scoreText": "4.0",
      "free": true,
      "price": 0,
      "currency": "USD",
      "playstoreUrl": "https://play.google.com/store/apps/details?id=com.instagram.android",
      "developer": "Instagram",
      "developerId": "Instagram"
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                         |
| ------------------------ | ------ | ------------------------------------------------------------------- |
| `data`                   | array  | Array of [Application summary objects](#application-summary-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                                    |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get Reviews

Returns reviews for an app. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).

**Request**

```bash
GET /google-play/reviews
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Default | Description                                                                                                 |
| ---------- | ------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------- |
| `appId`    | string  | Yes      | —       | Package name, max 255 characters, letters/digits/`.`/`_` only                                               |
| `country`  | string  | No       | —       | Exactly 2 characters. Not restricted to a fixed list.                                                       |
| `lang`     | string  | No       | —       | Language code for localized text, pattern `^[a-z]{2,3}(-[a-z]{2,3})?$`                                      |
| `sort`     | string  | No       | —       | `NEWEST`, `RATING`, or `HELPFULNESS`                                                                        |
| `maxItems` | integer | No       | —       | One-shot bulk pull, 1–500 results. Mutually exclusive with `cursor`                                         |
| `cursor`   | string  | No       | —       | Opaque pagination cursor from a previous response's `pagination.cursor`. Mutually exclusive with `maxItems` |

**Example**

```bash
curl -G "https://api.pullbay.com/google-play/reviews" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d appId=com.instagram.android \
  -d country=us \
  -d sort=NEWEST
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "045b1b4e-b207-40f7-8a9e-d4e81664965e",
      "userName": "Jane Doe",
      "userImage": "https://example.com/reviewer-photo.jpg",
      "date": "2025-01-12T22:37:55.959Z",
      "score": 1,
      "scoreText": "1",
      "url": "https://play.google.com/store/apps/details?id=com.instagram.android&reviewId=045b1b4e-b207-40f7-8a9e-d4e81664965e",
      "title": null,
      "text": "The new update is so buggy.",
      "replyDate": null,
      "replyText": null,
      "version": "362.0.0.33.241",
      "thumbsUp": 10224,
      "country": "US",
      "criterias": [
        { "criteria": "vaf_mark_message_as_read", "rating": 1 }
      ]
    }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                                                              | Type             | Description                                                                                 |
| ------------------------------------------------------------------ | ---------------- | ------------------------------------------------------------------------------------------- |
| `data`                                                             | array            | Array of [Review objects](#review-object)                                                   |
| `pagination.cursor`                                                | string, nullable | Cursor to pass as the `cursor` parameter on the next request, if more reviews are available |
| `pagination.page` / `pagination.hasNextPage` / `pagination.offset` | nullable         | Unused by this endpoint — always `null` here; use `pagination.cursor` instead               |
| `pricing.creditsCharged`                                           | number           | Credits charged for this request                                                            |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No flat per-page or per-review rate is published — read the figure from each response.

***

### Get Permissions

Returns the list of Android permissions the app requests, grouped by category.

**Request**

```bash
GET /google-play/permissions
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Default | Description                                                   |
| --------- | ------ | -------- | ------- | ------------------------------------------------------------- |
| `appId`   | string | Yes      | —       | Package name, max 255 characters, letters/digits/`.`/`_` only |

**Example**

```bash
curl -G "https://api.pullbay.com/google-play/permissions" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d appId=com.instagram.android
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    { "permission": "find accounts on the device", "type": "Identity" }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                       |
| ------------------------ | ------ | ------------------------------------------------- |
| `data`                   | array  | Array of [Permission objects](#permission-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                  |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get Data Safety

Returns the developer-declared Data Safety information: data shared, data collected, security practices, and privacy policy URL.

**Request**

```bash
GET /google-play/data-safety
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Default | Description                                                   |
| --------- | ------ | -------- | ------- | ------------------------------------------------------------- |
| `appId`   | string | Yes      | —       | Package name, max 255 characters, letters/digits/`.`/`_` only |

**Example**

```bash
curl -G "https://api.pullbay.com/google-play/data-safety" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d appId=com.instagram.android
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "sharedData": [
        { "data": "Name", "purpose": "Fraud prevention, security, and compliance", "type": "Personal info", "optional": false }
      ],
      "collectedData": [
        { "data": "Name", "purpose": "Fraud prevention, security, and compliance", "type": "Personal info", "optional": false }
      ],
      "securityPractices": [
        { "practice": "Data is encrypted in transit", "description": "Your data is transferred over a secure connection" }
      ],
      "privacyPolicyUrl": "http://instagram.com/legal/privacy/"
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                         |
| ------------------------ | ------ | ------------------------------------------------------------------- |
| `data`                   | array  | Array containing a single [Data Safety object](#data-safety-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                                    |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                                          |
| ------ | ---------------------------------------------------------------- |
| `400`  | Bad request — missing or invalid parameters                      |
| `402`  | Insufficient credits                                             |
| `404`  | App, review set, permission set, or Data Safety record not found |

## Object Schema

### Application object

| Field                                                                                       | Type                               | Description                                            |
| ------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------ |
| `title`                                                                                     | string                             | App name                                               |
| `appId`                                                                                     | string                             | Package name                                           |
| `url`                                                                                       | string                             | Relative API URL for the app detail endpoint           |
| `icon`                                                                                      | string                             | Icon image URL                                         |
| `headerImage`                                                                               | string                             | Feature graphic image URL                              |
| `summary`                                                                                   | string                             | Short tagline shown on the store listing               |
| `description` / `descriptionHTML`                                                           | string                             | Full description, plain text and HTML                  |
| `score` / `scoreText`                                                                       | number / string                    | Average rating, numeric and display string             |
| `ratings` / `reviews`                                                                       | integer                            | Total rating count and total review count              |
| `histogram`                                                                                 | object                             | Rating counts keyed by star value (`1`–`5`)            |
| `free` / `price` / `priceText` / `currency`                                                 | boolean / number / string / string | Pricing info                                           |
| `offersIAP`                                                                                 | boolean                            | Whether the app offers in-app purchases                |
| `adSupported`                                                                               | boolean                            | Whether the app shows ads                              |
| `installs` / `minInstalls` / `maxInstalls`                                                  | string / integer / integer         | Install count range, display string and numeric bounds |
| `androidVersion` / `androidVersionText`                                                     | string                             | Minimum Android version, raw and display form          |
| `updated`                                                                                   | integer                            | Last update timestamp, Unix milliseconds               |
| `genre` / `genreId`                                                                         | string                             | Category name and ID                                   |
| `developer` / `developerId` / `developerEmail` / `developerWebsite` / `developerInternalID` | string                             | Developer identity and contact info                    |
| `privacyPolicy`                                                                             | string                             | Developer's privacy policy URL                         |
| `playstoreUrl`                                                                              | string                             | Public Google Play listing URL                         |
| `screenshots`                                                                               | array                              | Screenshot image URLs                                  |

### Application summary object

Lightweight app reference returned by the search and similar-apps endpoints.

| Field                         | Type                      | Description                                  |
| ----------------------------- | ------------------------- | -------------------------------------------- |
| `title`                       | string                    | App name                                     |
| `appId`                       | string                    | Package name                                 |
| `url`                         | string                    | Relative API URL for the app detail endpoint |
| `icon`                        | string                    | Icon image URL                               |
| `summary`                     | string                    | Short tagline shown on the store listing     |
| `score` / `scoreText`         | number / string           | Average rating, numeric and display string   |
| `free` / `price` / `currency` | boolean / number / string | Pricing info                                 |
| `playstoreUrl`                | string                    | Public Google Play listing URL               |
| `developer` / `developerId`   | string                    | Developer name and ID                        |

### Review object

| Field                     | Type                                                        | Description                                                      |
| ------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------- |
| `id`                      | string                                                      | Review ID                                                        |
| `userName`                | string                                                      | Reviewer's display name                                          |
| `userImage`               | string                                                      | Reviewer's avatar URL                                            |
| `date`                    | string (date-time)                                          | When the review was posted                                       |
| `score` / `scoreText`     | integer / string                                            | Rating, 1–5, numeric and display string                          |
| `url`                     | string                                                      | Permalink to the review                                          |
| `title`                   | string, nullable                                            | Review title (often `null` on Google Play)                       |
| `text`                    | string                                                      | Review body                                                      |
| `replyDate` / `replyText` | string / string, nullable                                   | Developer's reply timestamp and text, if any                     |
| `version`                 | string                                                      | App version the review was left for                              |
| `thumbsUp`                | integer                                                     | Helpful-vote count                                               |
| `country`                 | string                                                      | Storefront country the review was posted on                      |
| `criterias`               | array of [Review criteria objects](#review-criteria-object) | Rating breakdown by sub-criteria, if the developer collects them |

### Review criteria object

| Field      | Type    | Description                                                            |
| ---------- | ------- | ---------------------------------------------------------------------- |
| `criteria` | string  | Internal name of the rated criterion (e.g. `vaf_mark_message_as_read`) |
| `rating`   | integer | Rating given for that specific criterion                               |

### Permission object

| Field        | Type   | Description                                                                |
| ------------ | ------ | -------------------------------------------------------------------------- |
| `permission` | string | Human-readable permission description (e.g. `find accounts on the device`) |
| `type`       | string | Permission category (e.g. `Identity`)                                      |

### Data Safety object

| Field               | Type                                                            | Description                            |
| ------------------- | --------------------------------------------------------------- | -------------------------------------- |
| `sharedData`        | array of [Data Safety item objects](#data-safety-item-object)   | Data types shared with third parties   |
| `collectedData`     | array of [Data Safety item objects](#data-safety-item-object)   | Data types collected by the app        |
| `securityPractices` | array of [Security practice objects](#security-practice-object) | Declared security practices            |
| `privacyPolicyUrl`  | string                                                          | Link to the developer's privacy policy |

### Data Safety item object

| Field      | Type    | Description                                       |
| ---------- | ------- | ------------------------------------------------- |
| `data`     | string  | Type of data (e.g. `Name`)                        |
| `purpose`  | string  | Stated purpose for collecting or sharing the data |
| `type`     | string  | Data category (e.g. `Personal info`)              |
| `optional` | boolean | Whether providing this data is optional           |

### Security practice object

| Field         | Type   | Description                                                         |
| ------------- | ------ | ------------------------------------------------------------------- |
| `practice`    | string | Name of the security practice (e.g. `Data is encrypted in transit`) |
| `description` | string | Explanation of the practice                                         |

## Finding App IDs

* **App ID**: the Android package name, visible in a Google Play URL, e.g. `https://play.google.com/store/apps/details?id=com.instagram.android` → `com.instagram.android`.
* Package names are dot-separated identifiers made up of letters, digits, periods, and underscores (pattern `^[A-Za-z0-9._]+$`) — there is no separate numeric ID like on the App Store.

## Country and Language Codes

* `country` is constrained by the spec only to exactly 2 characters — it isn't validated against a fixed allow-list, so pass a standard ISO 3166-1 alpha-2 storefront code (`us`, `gb`, `jp`, `de`, ...).
* `lang` matches the pattern `^[a-z]{2,3}(-[a-z]{2,3})?$` — a 2–3 letter language code, optionally with a region subtag (`en`, `en-us`, `pt-br`) — also not restricted to a fixed list.
* Neither parameter has a documented default in the API spec. If you omit them, behavior is determined by the underlying Google Play lookup rather than a guaranteed Pullbay default — pass both explicitly if you need a predictable storefront/locale.

## Python Example: Paginating Through Reviews

This example fetches reviews for an app a page at a time using cursor-based pagination, stopping when `pagination.cursor` comes back empty or a request cap is reached:

```python
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.pullbay.com"

def get_all_reviews(app_id, country="us", max_requests=10):
    all_reviews = []
    cursor = None
    requests_made = 0

    while requests_made < max_requests:
        params = {"appId": app_id, "country": country, "sort": "NEWEST"}
        if cursor:
            params["cursor"] = cursor

        resp = requests.get(
            f"{BASE_URL}/google-play/reviews",
            headers={"Authorization": f"Bearer {API_KEY}"},
            params=params,
        )
        resp.raise_for_status()
        body = resp.json()
        all_reviews.extend(body["data"])
        requests_made += 1

        cursor = body.get("pagination", {}).get("cursor")
        if not cursor:
            break

    return all_reviews

reviews = get_all_reviews("com.instagram.android")
print(f"Fetched {len(reviews)} reviews")
```

Each request is billed independently, with the charged amount reported in that response's `pricing.creditsCharged` — there's no separate "fetch everything in one call" endpoint, so a multi-page pull like this makes one request per cursor advance. Alternatively, pass `maxItems` (1–500) instead of paginating with `cursor` if a single bulk pull is enough for your use case.

## FAQ

<details>

<summary>How do I find an app's package name?</summary>

Visit the app's page on the Google Play Store. Look at the URL — it contains `?id=` followed by the package name. For example, `https://play.google.com/store/apps/details?id=com.instagram.android` → `com.instagram.android`.

</details>

<details>

<summary>What countries are supported?</summary>

Any 2-letter code is accepted by the API — it isn't restricted to a published list of supported countries.

</details>

<details>

<summary>How many reviews can I fetch in one call?</summary>

Up to 500 with `maxItems`, or page through with `cursor` (page size isn't fixed by the spec). `cursor` and `maxItems` are mutually exclusive — use one or the other per request.

</details>

<details>

<summary>Can I filter reviews by star rating?</summary>

The API only supports sorting (`NEWEST`, `RATING`, or `HELPFULNESS`), not server-side filtering by rating. Fetch the reviews you need and filter client-side on the `score` field.

</details>

<details>

<summary>What's the credit cost per request?</summary>

It's dynamic — every response includes `pricing.creditsCharged` with the exact amount for that call. There's no fixed published rate per endpoint or per result.

</details>

<details>

<summary>Can I check what data an app collects before integrating with it?</summary>

Yes — use Get Permissions for the Android permissions the app requests, and Get Data Safety for the developer's declared data collection, sharing, and security practices.

</details>

## Next Steps

* [**API Integration Guide**](broken://pages/68e75ab1a73d266350bd68301be87ff97cad563e) — integrate the Pullbay API into your backend application.
* [**n8n Integration Guide**](broken://pages/4308164455b56bbc694cf74c9768ea689c2fe719) — set up automated workflows using n8n with no coding required.
* [**Services Overview**](broken://pages/0d55376cbcca70097a683a71a593d3077825dbf1) — explore all available Pullbay services.


# Google Search

The Pullbay Google Search API provides programmatic access to Google web search results. Integrate SERP data into your applications, dashboards, and analytics pipelines without scraping or managing your own search infrastructure.

**Base URL:** `https://api.pullbay.com`

## Overview

The Google Search service lets you:

* Run a Google web search for any query string
* Page through results with `page`, or pull a larger batch of results in one request with `maxItems`
* Pass through a handful of optional, loosely-specified query modifiers (`safe`, `hl`, `country`, `noEncode`) supported by the underlying search

This is a single-endpoint service: there is one `GET` request that returns Google's organic search results for a query.

## Common Use Cases

**SEO Rank Tracking** — Run the same query on a schedule and track how a domain's position in `data` moves over time.

**SERP Research** — Pull the current result set for a keyword to see which domains, titles, and snippets currently rank.

**Content & Competitor Research** — Search topics relevant to your niche and mine the `title`/`snippet` fields for how competitors position themselves.

**Brand Monitoring** — Search for your brand or product name periodically and check which pages and snippets show up.

**Data Pipelines** — Feed Google search results into a downstream analytics, alerting, or BI pipeline instead of scraping search result pages directly.

## Endpoints

| Endpoint                    | Description               | Pagination          |
| --------------------------- | ------------------------- | ------------------- |
| `GET /google-search/search` | Search the web via Google | `page` / `maxItems` |

***

### Search

Returns Google search results for `query`.

**Request**

```bash
GET /google-search/search
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Default | Description                                                                                                                                      |
| ---------- | ------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `query`    | string  | Yes      | —       | Search query, 1–1000 characters                                                                                                                  |
| `safe`     | string  | No       | —       | Safe-search filter string. The API spec doesn't document a restricted set of accepted values for this parameter.                                 |
| `hl`       | string  | No       | —       | Language code string. The API spec doesn't document a restricted set of accepted values for this parameter.                                      |
| `country`  | string  | No       | —       | Country/region string. The API spec doesn't document a restricted set of accepted values for this parameter.                                     |
| `count`    | string  | No       | —       | Result-count selector. This is a closed enum — the only currently accepted value is `"10"`. The spec does not document any other accepted value. |
| `noEncode` | string  | No       | —       | String parameter. The API spec doesn't document a restricted set of accepted values for this parameter.                                          |
| `maxItems` | integer | No       | —       | One-shot bulk pull, 1–200 results                                                                                                                |
| `page`     | integer | No       | —       | Page number, 1 and up (the documented maximum is effectively unbounded)                                                                          |

Note: `page` and `maxItems` are both listed as independent optional parameters on this endpoint. The spec does not document a mutual-exclusivity rule between them — use `page` for sequential page-based navigation, or `maxItems` to request a larger batch of results in a single call.

**Example**

```bash
curl -G "https://api.pullbay.com/google-search/search" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d query="openai" \
  -d count=10 \
  -d page=1
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "searchResult",
      "title": "OpenAI | OpenAI",
      "link": "https://openai.com/",
      "snippet": "We believe our research will eventually lead to artificial general intelligence, a system that can solve human-level problems."
    }
  ],
  "pagination": { "page": 1, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                                     | Type              | Description                                                       |
| ----------------------------------------- | ----------------- | ----------------------------------------------------------------- |
| `data`                                    | array             | Array of [GoogleSearchResult objects](#googlesearchresult-object) |
| `pagination.page`                         | integer, nullable | Current page number (only set when using `page`)                  |
| `pagination.hasNextPage`                  | boolean, nullable | Whether another page is available                                 |
| `pagination.cursor` / `pagination.offset` | string, nullable  | Not used by this endpoint                                         |
| `pricing.creditsCharged`                  | number            | Credits charged for this request                                  |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                     |
| ------ | ------------------------------------------- |
| `400`  | Bad request — missing or invalid parameters |
| `402`  | Insufficient credits                        |
| `404`  | Not found                                   |

## Object Schema

### GoogleSearchResult object

| Field     | Type         | Description                                 |
| --------- | ------------ | ------------------------------------------- |
| `type`    | string       | Result type, e.g. `searchResult`            |
| `title`   | string       | Title of the search result                  |
| `link`    | string (uri) | URL of the search result                    |
| `snippet` | string       | Text snippet shown beneath the result title |

Note: as documented, the GoogleSearchResult object contains only these four fields — there is no position/rank field, no rich-result metadata, and no sitelinks in the schema.

## Python Example: Paginating Through Search Results

This example pages through results using `page`, stopping when a page comes back with no results or `pagination.hasNextPage` is explicitly `false`. The check for an empty page matters here because, as the sample response above shows, `hasNextPage` can come back as `null` rather than a clean boolean:

```python
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.pullbay.com"

def get_all_results(query, max_pages=10):
    all_results = []
    page = 1
    while page <= max_pages:
        resp = requests.get(
            f"{BASE_URL}/google-search/search",
            headers={"Authorization": f"Bearer {API_KEY}"},
            params={"query": query, "page": page},
        )
        resp.raise_for_status()
        body = resp.json()
        results = body.get("data", [])
        if not results:
            break
        all_results.extend(results)
        if body.get("pagination", {}).get("hasNextPage") is False:
            break
        page += 1
    return all_results

results = get_all_results("openai")
print(f"Fetched {len(results)} results")
```

Each request is billed independently, with the charged amount reported in that response's `pricing.creditsCharged`. If you'd rather pull a larger batch in one call instead of looping over `page`, pass `maxItems` (1–200) on a single request.

## FAQ

<details>

<summary>How many results come back per page?</summary>

The spec exposes a `count` parameter, but it's a closed enum whose only currently documented value is `"10"`. No other value is documented as accepted.

</details>

<details>

<summary>What do <code>safe</code>, <code>hl</code>, <code>country</code>, and <code>noEncode</code> accept?</summary>

The API spec doesn't document a restricted set of accepted values (no enum, no pattern) for any of these four parameters — they're plain optional strings with no further documented constraint. Pullbay's spec does not publish an allow-list for any of them.

</details>

<details>

<summary>Can I use <code>page</code> and <code>maxItems</code> together?</summary>

The spec doesn't document a rule against it either way — it lists them as two independent optional parameters. `page` is for stepping through results sequentially; `maxItems` is for pulling a larger batch of results in one shot.

</details>

<details>

<summary>What's the credit cost per request?</summary>

It's dynamic — every response includes `pricing.creditsCharged` with the exact amount for that call. There's no fixed published rate.

</details>

<details>

<summary>Does the response tell me the total number of matching results?</summary>

No — the response envelope only includes `data`, `pagination`, and `pricing`. There's no total-result-count field documented for this endpoint.

</details>

<details>

<summary>Can I search images, news, or videos with this endpoint?</summary>

No — this endpoint returns standard web search results (`GoogleSearchResult` objects only). There's no separate mode or parameter documented for image, news, or video search.

</details>


# Instagram

The Pullbay Instagram API provides programmatic access to public Instagram profile, post, reel, story, and comment data. Resolve handles to user IDs, pull a profile, list a user's posts/tagged posts/reels, read active stories, fetch a single post by shortcode, or pull comments on a post — all without scraping or owning an Instagram account of your own.

**Base URL:** `https://api.pullbay.com`

## Overview

The Instagram service lets you:

* Resolve a handle (`@username`) to Instagram's internal numeric user ID
* Look up a full user profile by handle or by numeric ID
* List a user's posts, posts they're tagged in, and reels (cursor-paginated)
* Fetch a user's currently-active stories
* Fetch a single post by its shortcode (the code in an `instagram.com/p/{code}/` URL)
* Fetch comments on a post by the post's internal ID (cursor-paginated)

All endpoints are read-only `GET` requests against public Instagram data — no account of your own is required. This service uses **three distinct identifier types that are not interchangeable**: `handle` (the `@username`, passed without the `@`), `id` (Instagram's internal numeric ID for a user, post, or comment), and `code` (a post's shortcode, the segment after `/p/` in a post URL). Always check which one a given endpoint expects.

## Common Use Cases

**Influencer Vetting** — Pull a creator's profile (follower count, post count, verification status) before negotiating a sponsorship, and check their recent posts for content fit and engagement levels.

**Engagement Tracking** — Poll a brand or creator account's posts and reels on a schedule to track `likeCount` and `commentCount` trends over time.

**Campaign Monitoring** — Fetch posts a brand account is tagged in to see how a hashtag or partnership campaign is performing across other accounts.

**Comment Sentiment Analysis** — Pull comments on a post and feed the `message` field into your own sentiment or moderation pipeline.

**Story Archiving** — Capture a user's active stories before they expire (24 hours after posting) for compliance, reporting, or content-repurposing workflows.

## Endpoints

| Endpoint                                | Description                           | Pagination        |
| --------------------------------------- | ------------------------------------- | ----------------- |
| `GET /instagram/userid/{handle}`        | Resolve a handle to a numeric user ID | —                 |
| `GET /instagram/user/handle/{handle}`   | Get a user profile by handle          | —                 |
| `GET /instagram/user/{id}`              | Get a user profile by numeric ID      | —                 |
| `GET /instagram/user/{id}/posts`        | Get posts published by a user         | Cursor / maxItems |
| `GET /instagram/user/{id}/posts/tagged` | Get posts a user is tagged in         | Cursor / maxItems |
| `GET /instagram/user/{id}/reels`        | Get reels published by a user         | Cursor / maxItems |
| `GET /instagram/user/{id}/stories`      | Get a user's currently-active stories | —                 |
| `GET /instagram/post/code/{code}`       | Get a single post by shortcode        | —                 |
| `GET /instagram/post/{id}/comments`     | Get comments on a post                | Cursor / maxItems |

***

### Resolve User ID

Returns whether a handle is available and, if it exists, the resolved Instagram user ID.

**Request**

```http
GET /instagram/userid/{handle}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type          | Required | Default | Description                                                        |
| --------- | ------------- | -------- | ------- | ------------------------------------------------------------------ |
| `handle`  | string (path) | Yes      | —       | Instagram handle, without `@`, 1–100 characters (e.g. `instagram`) |

**Example**

```bash
curl -G "https://api.pullbay.com/instagram/userid/instagram" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "isAvailable": true,
      "handle": "instagram",
      "id": "25025320"
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                               |
| ------------------------ | ------ | ------------------------------------------------------------------------- |
| `data`                   | array  | Array containing a single [User ID lookup object](#user-id-lookup-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                                          |

**Credit cost:** charged dynamically per request; the exact amount is always returned in `pricing.creditsCharged`. No published flat rate — read the figure from the response.

***

### Get User By Handle

Returns an Instagram user profile by handle (without `@`).

**Request**

```http
GET /instagram/user/handle/{handle}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type          | Required | Default | Description                                                        |
| --------- | ------------- | -------- | ------- | ------------------------------------------------------------------ |
| `handle`  | string (path) | Yes      | —       | Instagram handle, without `@`, 1–100 characters (e.g. `instagram`) |

**Example**

```bash
curl -G "https://api.pullbay.com/instagram/user/handle/instagram" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "user",
      "isAvailable": true,
      "id": "25025320",
      "username": "instagram",
      "fullName": "Instagram",
      "biography": "Discover what's new on Instagram 🔎✨",
      "profilePicUrl": "https://instagram.com/static/images/profile.jpg",
      "website": "https://about.instagram.com",
      "category": "App page",
      "cityName": null,
      "isPrivate": false,
      "isVerified": true,
      "isBusiness": true,
      "hasVideos": true,
      "hasGuides": false,
      "postCount": 8200,
      "followerCount": 689000000,
      "followingCount": 264,
      "totalReelsCount": 1850,
      "bioLinks": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                           |
| ------------------------ | ------ | ----------------------------------------------------- |
| `data`                   | array  | Array containing a single [User object](#user-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                      |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Get User By ID

Returns an Instagram user profile by numeric user ID.

**Request**

```http
GET /instagram/user/{id}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type          | Required | Default | Description                                                   |
| --------- | ------------- | -------- | ------- | ------------------------------------------------------------- |
| `id`      | string (path) | Yes      | —       | Numeric Instagram user ID, 1–100 characters (e.g. `25025320`) |

**Example**

```bash
curl -G "https://api.pullbay.com/instagram/user/25025320" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "user",
      "isAvailable": true,
      "id": "25025320",
      "username": "instagram",
      "fullName": "Instagram",
      "biography": "Discover what's new on Instagram 🔎✨",
      "profilePicUrl": "https://instagram.com/static/images/profile.jpg",
      "website": "https://about.instagram.com",
      "category": "App page",
      "cityName": null,
      "isPrivate": false,
      "isVerified": true,
      "isBusiness": true,
      "hasVideos": true,
      "hasGuides": false,
      "postCount": 8200,
      "followerCount": 689000000,
      "followingCount": 264,
      "totalReelsCount": 1850,
      "bioLinks": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                           |
| ------------------------ | ------ | ----------------------------------------------------- |
| `data`                   | array  | Array containing a single [User object](#user-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                      |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Get User Posts

Returns posts published by an Instagram user. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).

**Request**

```http
GET /instagram/user/{id}/posts
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type            | Required | Default | Description                                                                    |
| ---------- | --------------- | -------- | ------- | ------------------------------------------------------------------------------ |
| `id`       | string (path)   | Yes      | —       | Numeric Instagram user ID, 1–100 characters                                    |
| `maxItems` | integer (query) | No       | —       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`            |
| `cursor`   | string (query)  | No       | —       | Pagination cursor from a previous response. Mutually exclusive with `maxItems` |

**Example**

```bash
curl -G "https://api.pullbay.com/instagram/user/25025320/posts" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d maxItems=50
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "photo",
      "id": "3881985252256633854",
      "code": "CzABCdef123",
      "url": "https://www.instagram.com/p/CzABCdef123/",
      "caption": "Check out our latest update!",
      "createdAt": 1714560000,
      "likeCount": 12500,
      "commentCount": 320,
      "isAvailable": true,
      "isCarousel": false,
      "isVideo": false,
      "isPinned": false,
      "isPaidPartnership": false,
      "isLikeAndViewCountsDisabled": false,
      "owner": {
        "id": "25025320",
        "username": "instagram",
        "fullName": "Instagram",
        "profilePicUrl": "https://instagram.com/static/images/profile.jpg",
        "isVerified": true,
        "isPrivate": false
      },
      "coowners": [],
      "location": null,
      "image": {},
      "video": null,
      "audio": null,
      "carouselMedia": []
    }
  ],
  "pagination": { "page": null, "hasNextPage": true, "cursor": "QVFE...", "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                                   | Type              | Description                                    |
| --------------------------------------- | ----------------- | ---------------------------------------------- |
| `data`                                  | array             | Array of [Post objects](#post-object)          |
| `pagination.hasNextPage`                | boolean, nullable | Whether another page is available              |
| `pagination.cursor`                     | string, nullable  | Cursor to pass as `cursor` on the next request |
| `pagination.page` / `pagination.offset` | nullable          | Unused by this endpoint — always `null` here   |
| `pricing.creditsCharged`                | number            | Credits charged for this request               |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate — read the figure from each response.

***

### Get User Tagged Posts

Returns posts where the Instagram user is tagged. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive).

**Request**

```http
GET /instagram/user/{id}/posts/tagged
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type            | Required | Default | Description                                                                    |
| ---------- | --------------- | -------- | ------- | ------------------------------------------------------------------------------ |
| `id`       | string (path)   | Yes      | —       | Numeric Instagram user ID, 1–100 characters                                    |
| `maxItems` | integer (query) | No       | —       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`            |
| `cursor`   | string (query)  | No       | —       | Pagination cursor from a previous response. Mutually exclusive with `maxItems` |

**Example**

```bash
curl -G "https://api.pullbay.com/instagram/user/25025320/posts/tagged" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d cursor="QVFE..."
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "photo",
      "id": "3881985252256633900",
      "code": "CzXYZdef456",
      "url": "https://www.instagram.com/p/CzXYZdef456/",
      "caption": "Great day at the launch event!",
      "createdAt": 1714646400,
      "likeCount": 4300,
      "commentCount": 95,
      "isAvailable": true,
      "isCarousel": false,
      "isVideo": false,
      "isPinned": false,
      "isPaidPartnership": false,
      "isLikeAndViewCountsDisabled": false,
      "owner": {
        "id": "7894561230",
        "username": "anotheruser",
        "fullName": "Another User",
        "profilePicUrl": "https://instagram.com/static/images/profile2.jpg",
        "isVerified": false,
        "isPrivate": false
      },
      "coowners": [],
      "location": null,
      "image": {},
      "video": null,
      "audio": null,
      "carouselMedia": []
    }
  ],
  "pagination": { "page": null, "hasNextPage": true, "cursor": "QVFE...", "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                                   | Type              | Description                                    |
| --------------------------------------- | ----------------- | ---------------------------------------------- |
| `data`                                  | array             | Array of [Post objects](#post-object)          |
| `pagination.hasNextPage`                | boolean, nullable | Whether another page is available              |
| `pagination.cursor`                     | string, nullable  | Cursor to pass as `cursor` on the next request |
| `pagination.page` / `pagination.offset` | nullable          | Unused by this endpoint — always `null` here   |
| `pricing.creditsCharged`                | number            | Credits charged for this request               |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Get User Reels

Returns reels published by an Instagram user. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive). Reels are returned using the same [Post object](#post-object) schema as the posts endpoints — there is no separate "reel" schema.

**Request**

```http
GET /instagram/user/{id}/reels
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type            | Required | Default | Description                                                                    |
| ---------- | --------------- | -------- | ------- | ------------------------------------------------------------------------------ |
| `id`       | string (path)   | Yes      | —       | Numeric Instagram user ID, 1–100 characters                                    |
| `maxItems` | integer (query) | No       | —       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`            |
| `cursor`   | string (query)  | No       | —       | Pagination cursor from a previous response. Mutually exclusive with `maxItems` |

**Example**

```bash
curl -G "https://api.pullbay.com/instagram/user/25025320/reels" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d maxItems=25
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "reel",
      "id": "3881985252256634000",
      "code": "CzREELabc789",
      "url": "https://www.instagram.com/p/CzREELabc789/",
      "caption": "New reel just dropped 🎥",
      "createdAt": 1714732800,
      "likeCount": 98000,
      "commentCount": 1820,
      "isAvailable": true,
      "isCarousel": false,
      "isVideo": true,
      "isPinned": false,
      "isPaidPartnership": false,
      "isLikeAndViewCountsDisabled": false,
      "owner": {
        "id": "25025320",
        "username": "instagram",
        "fullName": "Instagram",
        "profilePicUrl": "https://instagram.com/static/images/profile.jpg",
        "isVerified": true,
        "isPrivate": false
      },
      "coowners": [],
      "location": null,
      "image": null,
      "video": {},
      "audio": {},
      "carouselMedia": []
    }
  ],
  "pagination": { "page": null, "hasNextPage": true, "cursor": "QVFE...", "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                                   | Type              | Description                                                          |
| --------------------------------------- | ----------------- | -------------------------------------------------------------------- |
| `data`                                  | array             | Array of [Post objects](#post-object) (returned with `type: "reel"`) |
| `pagination.hasNextPage`                | boolean, nullable | Whether another page is available                                    |
| `pagination.cursor`                     | string, nullable  | Cursor to pass as `cursor` on the next request                       |
| `pagination.page` / `pagination.offset` | nullable          | Unused by this endpoint — always `null` here                         |
| `pricing.creditsCharged`                | number            | Credits charged for this request                                     |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Get User Stories

Returns the user's currently-active Instagram stories. Not paginated — stories expire 24 hours after posting, so the full active set is always returned in one call.

**Request**

```http
GET /instagram/user/{id}/stories
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type          | Required | Default | Description                                 |
| --------- | ------------- | -------- | ------- | ------------------------------------------- |
| `id`      | string (path) | Yes      | —       | Numeric Instagram user ID, 1–100 characters |

**Example**

```bash
curl -G "https://api.pullbay.com/instagram/user/25025320/stories" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "video",
      "id": "3245678901234567890",
      "code": "CzABCdef123",
      "createdAt": 1714560000,
      "expiringAt": 1714646400,
      "duration": 14.97,
      "hasAudio": true,
      "hasTranslation": false,
      "canReshare": true,
      "canReply": true,
      "isVideo": true,
      "owner": {
        "id": "25025320",
        "username": "instagram",
        "fullName": "Instagram",
        "profilePicUrl": "https://instagram.com/static/images/profile.jpg",
        "isVerified": true,
        "isPrivate": false
      },
      "coowners": [],
      "image": null,
      "video": {},
      "audio": {},
      "link": null,
      "comments": [],
      "mentions": [],
      "hashtags": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                             |
| ------------------------ | ------ | --------------------------------------- |
| `data`                   | array  | Array of [Story objects](#story-object) |
| `pricing.creditsCharged` | number | Credits charged for this request        |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Get Post By Shortcode

Returns a single Instagram post by its shortcode (the segment after `/p/` in the post's URL).

**Request**

```http
GET /instagram/post/code/{code}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type          | Required | Default | Description                                                                                       |
| --------- | ------------- | -------- | ------- | ------------------------------------------------------------------------------------------------- |
| `code`    | string (path) | Yes      | —       | Post shortcode, 1–100 characters (e.g. `CzABCdef123`, the code in `instagram.com/p/CzABCdef123/`) |

**Example**

```bash
curl -G "https://api.pullbay.com/instagram/post/code/CzABCdef123" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "photo",
      "id": "3881985252256633854",
      "code": "CzABCdef123",
      "url": "https://www.instagram.com/p/CzABCdef123/",
      "caption": "Check out our latest update!",
      "createdAt": 1714560000,
      "likeCount": 12500,
      "commentCount": 320,
      "isAvailable": true,
      "isCarousel": false,
      "isVideo": false,
      "isPinned": false,
      "isPaidPartnership": false,
      "isLikeAndViewCountsDisabled": false,
      "owner": {
        "id": "25025320",
        "username": "instagram",
        "fullName": "Instagram",
        "profilePicUrl": "https://instagram.com/static/images/profile.jpg",
        "isVerified": true,
        "isPrivate": false
      },
      "coowners": [],
      "location": null,
      "image": {},
      "video": null,
      "audio": null,
      "carouselMedia": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                           |
| ------------------------ | ------ | ----------------------------------------------------- |
| `data`                   | array  | Array containing a single [Post object](#post-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                      |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Get Post Comments

Returns comments on an Instagram post. Use `cursor` to paginate or `maxItems` for a one-shot bulk pull (mutually exclusive). Takes the post's internal numeric `id`, **not** its shortcode.

**Request**

```http
GET /instagram/post/{id}/comments
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type            | Required | Default | Description                                                                    |
| ---------- | --------------- | -------- | ------- | ------------------------------------------------------------------------------ |
| `id`       | string (path)   | Yes      | —       | Numeric Instagram post ID, 1–100 characters (not the post's shortcode)         |
| `maxItems` | integer (query) | No       | —       | One-shot bulk pull, 1–500 results. Mutually exclusive with `cursor`            |
| `cursor`   | string (query)  | No       | —       | Pagination cursor from a previous response. Mutually exclusive with `maxItems` |

**Example**

```bash
curl -G "https://api.pullbay.com/instagram/post/3881985252256633854/comments" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d maxItems=100
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "comment",
      "id": "18470084392099954",
      "userId": "53847071613",
      "message": "Love this!",
      "createdAt": "2026-05-05T19:46:32.000Z",
      "likeCount": 27,
      "replyCount": 0,
      "isRanked": false,
      "user": {
        "id": "53847071613",
        "username": "someuser",
        "fullName": "Some User",
        "profilePicUrl": "https://instagram.com/static/images/profile3.jpg",
        "isPrivate": false,
        "isVerified": false
      }
    }
  ],
  "pagination": { "page": null, "hasNextPage": true, "cursor": "QVFE...", "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                                   | Type              | Description                                    |
| --------------------------------------- | ----------------- | ---------------------------------------------- |
| `data`                                  | array             | Array of [Comment objects](#comment-object)    |
| `pagination.hasNextPage`                | boolean, nullable | Whether another page is available              |
| `pagination.cursor`                     | string, nullable  | Cursor to pass as `cursor` on the next request |
| `pagination.page` / `pagination.offset` | nullable          | Unused by this endpoint — always `null` here   |
| `pricing.creditsCharged`                | number            | Credits charged for this request               |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                  |
| ------ | ---------------------------------------- |
| `400`  | Bad request — missing/invalid parameters |
| `402`  | Insufficient credits                     |
| `404`  | User, post, or story not found           |

## Object Schema

### User ID lookup object

| Field         | Type    | Description                                                          |
| ------------- | ------- | -------------------------------------------------------------------- |
| `isAvailable` | boolean | Whether the handle exists/is in use                                  |
| `handle`      | string  | The handle that was looked up                                        |
| `id`          | string  | Numeric Instagram user ID. Present only when `isAvailable` is `true` |

### User object

| Field             | Type             | Description                               |
| ----------------- | ---------------- | ----------------------------------------- |
| `type`            | string           | Always `user`                             |
| `isAvailable`     | boolean          | Whether the profile is available          |
| `id`              | string           | Numeric Instagram user ID                 |
| `username`        | string           | Handle, without `@`                       |
| `fullName`        | string           | Display name                              |
| `biography`       | string           | Bio text                                  |
| `profilePicUrl`   | string           | Profile picture URL                       |
| `website`         | string           | Link in bio                               |
| `category`        | string           | Business/category label (e.g. `App page`) |
| `cityName`        | string, nullable | City shown on profile                     |
| `isPrivate`       | boolean          | Whether the account is private            |
| `isVerified`      | boolean          | Whether the account has a verified badge  |
| `isBusiness`      | boolean          | Whether the account is a business account |
| `hasVideos`       | boolean          | Whether the account has published videos  |
| `hasGuides`       | boolean          | Whether the account has published guides  |
| `postCount`       | integer          | Total post count                          |
| `followerCount`   | integer          | Follower count                            |
| `followingCount`  | integer          | Following count                           |
| `totalReelsCount` | integer          | Total reel count                          |
| `bioLinks`        | array            | Links shown in the bio                    |

### Post owner object

Lightweight user reference embedded in posts and stories.

| Field           | Type    | Description                              |
| --------------- | ------- | ---------------------------------------- |
| `id`            | string  | Numeric Instagram user ID                |
| `username`      | string  | Handle, without `@`                      |
| `fullName`      | string  | Display name                             |
| `profilePicUrl` | string  | Profile picture URL                      |
| `isVerified`    | boolean | Whether the account has a verified badge |
| `isPrivate`     | boolean | Whether the account is private           |

### Post object

Used for posts, tagged posts, and reels — reels are returned with `type: "reel"` but share this same schema; there is no separate reel object.

| Field                         | Type             | Description                                                                     |
| ----------------------------- | ---------------- | ------------------------------------------------------------------------------- |
| `type`                        | string           | `photo` / `video` / `carousel` / `reel`                                         |
| `id`                          | string           | Numeric post ID                                                                 |
| `code`                        | string           | Shortcode (the segment after `/p/` in the URL)                                  |
| `url`                         | string           | Permalink to the post                                                           |
| `caption`                     | string           | Caption text                                                                    |
| `createdAt`                   | integer          | Unix timestamp (seconds) the post was published                                 |
| `likeCount`                   | integer          | Like count                                                                      |
| `commentCount`                | integer          | Comment count                                                                   |
| `isAvailable`                 | boolean          | Whether the post is still available                                             |
| `isCarousel`                  | boolean          | Whether the post is a multi-media carousel                                      |
| `isVideo`                     | boolean          | Whether the post is a video                                                     |
| `isPinned`                    | boolean          | Whether the post is pinned to the profile                                       |
| `isPaidPartnership`           | boolean          | Whether the post is a paid partnership                                          |
| `isLikeAndViewCountsDisabled` | boolean          | Whether like/view counts are hidden                                             |
| `owner`                       | object           | [Post owner object](#post-owner-object)                                         |
| `coowners`                    | array            | Additional tagged collaborators, each a [post owner object](#post-owner-object) |
| `location`                    | object, nullable | Tagged location                                                                 |
| `image`                       | object, nullable | Image media data                                                                |
| `video`                       | object, nullable | Video media data                                                                |
| `audio`                       | object, nullable | Audio track data                                                                |
| `carouselMedia`               | array            | Per-slide media for carousel posts                                              |

### Comment object

| Field        | Type               | Description                                                                                    |
| ------------ | ------------------ | ---------------------------------------------------------------------------------------------- |
| `type`       | string             | Always `comment`                                                                               |
| `id`         | string             | Comment ID                                                                                     |
| `userId`     | string             | Numeric ID of the commenter                                                                    |
| `message`    | string             | Comment text                                                                                   |
| `createdAt`  | string (date-time) | When the comment was posted                                                                    |
| `likeCount`  | integer            | Like count                                                                                     |
| `replyCount` | integer            | Reply count                                                                                    |
| `isRanked`   | boolean            | Whether the comment is algorithmically ranked/pinned                                           |
| `user`       | object             | Commenter reference (`id`, `username`, `fullName`, `profilePicUrl`, `isPrivate`, `isVerified`) |

### Story object

| Field            | Type             | Description                                                                     |
| ---------------- | ---------------- | ------------------------------------------------------------------------------- |
| `type`           | string           | `photo` / `video`                                                               |
| `id`             | string           | Story ID                                                                        |
| `code`           | string           | Shortcode                                                                       |
| `createdAt`      | integer          | Unix timestamp (seconds) the story was posted                                   |
| `expiringAt`     | integer          | Unix timestamp (seconds) the story expires                                      |
| `duration`       | number           | Duration in seconds                                                             |
| `hasAudio`       | boolean          | Whether the story has audio                                                     |
| `hasTranslation` | boolean          | Whether translated text is available                                            |
| `canReshare`     | boolean          | Whether the story can be reshared                                               |
| `canReply`       | boolean          | Whether replies are allowed                                                     |
| `isVideo`        | boolean          | Whether the story is a video                                                    |
| `owner`          | object           | [Post owner object](#post-owner-object)                                         |
| `coowners`       | array            | Additional tagged collaborators, each a [post owner object](#post-owner-object) |
| `image`          | object, nullable | Image media data                                                                |
| `video`          | object, nullable | Video media data                                                                |
| `audio`          | object, nullable | Audio track data                                                                |
| `link`           | object, nullable | Attached link sticker data                                                      |
| `comments`       | array            | Comments left on the story                                                      |
| `mentions`       | array            | Mentioned accounts                                                              |
| `hashtags`       | array            | Tagged hashtags                                                                 |

## Finding User IDs and Handles

This service uses three distinct identifiers — keep them separate:

* **`handle`**: the `@username` shown on a profile, passed without the `@` (e.g. `instagram` for `@instagram`). Used only by `GET /instagram/userid/{handle}` and `GET /instagram/user/handle/{handle}`.
* **`id`**: Instagram's internal numeric ID for a user, post, or comment. Required by every other endpoint in this service (posts, tagged posts, reels, stories, and post comments all key off a numeric `id`, not a handle or shortcode).
* **`code`**: a post's shortcode — the segment after `/p/` in a post's URL (e.g. `CzABCdef123` in `https://www.instagram.com/p/CzABCdef123/`). Used only by `GET /instagram/post/code/{code}`.

{% stepper %}
{% step %}

### If you only have a handle and need a numeric user `id`

Call `GET /instagram/userid/{handle}` to resolve the handle and confirm `isAvailable` is `true`. The response's `id` field is the numeric user ID.
{% endstep %}

{% step %}

### Use that `id` for the user endpoints

Use that `id` for the posts, tagged posts, reels, and stories endpoints (`/instagram/user/{id}/...`).
{% endstep %}
{% endstepper %}

If you need the full profile anyway, `GET /instagram/user/handle/{handle}` returns both the profile and the numeric `id` in a single call, saving a round trip.

A post's `id` (needed for the comments endpoint) is not the same as its `code` (shortcode). If you only have a post's shortcode, call `GET /instagram/post/code/{code}` first — the response's `id` field is the numeric post ID to use with `GET /instagram/post/{id}/comments`.

## Python Example

This example resolves a handle to a user ID, then paginates through that user's posts using `cursor`:

```python
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.pullbay.com"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

def resolve_handle(handle):
    resp = requests.get(f"{BASE_URL}/instagram/userid/{handle}", headers=HEADERS)
    resp.raise_for_status()
    body = resp.json()
    lookup = body["data"][0]
    if not lookup["isAvailable"]:
        raise ValueError(f"Handle '{handle}' is not available")
    return lookup["id"]

def get_all_posts(user_id, max_pages=10):
    all_posts = []
    cursor = None
    for _ in range(max_pages):
        params = {"cursor": cursor} if cursor else {}
        resp = requests.get(
            f"{BASE_URL}/instagram/user/{user_id}/posts",
            headers=HEADERS,
            params=params,
        )
        resp.raise_for_status()
        body = resp.json()
        all_posts.extend(body["data"])
        pagination = body.get("pagination", {})
        if not pagination.get("hasNextPage"):
            break
        cursor = pagination.get("cursor")
    return all_posts

user_id = resolve_handle("instagram")
posts = get_all_posts(user_id)
print(f"Fetched {len(posts)} posts for user {user_id}")
```

Each request is billed independently, with the charged amount reported in that response's `pricing.creditsCharged` — there's no separate "fetch everything in one call" endpoint, so a multi-page pull like this makes one request per page.

## FAQ

<details>

<summary>What's the difference between <code>handle</code>, <code>id</code>, and <code>code</code>?</summary>

`handle` is the `@username` (without the `@`), used only to resolve or look up a profile. `id` is Instagram's internal numeric identifier for a user, post, or comment, and is required by every other endpoint. `code` is a post's shortcode from its URL, used only to fetch a post by `GET /instagram/post/code/{code}`. They are not interchangeable — passing a handle where an `id` is expected (or vice versa) will not resolve.

</details>

<details>

<summary>How do I get a numeric user ID if I only have a handle?</summary>

Call `GET /instagram/userid/{handle}` and read the `id` field from the response, or call `GET /instagram/user/handle/{handle}` if you also want the full profile in the same request.

</details>

<details>

<summary>How do I get a post's numeric ID if I only have its shortcode?</summary>

Call `GET /instagram/post/code/{code}`. The returned [Post object](#post-object) includes the post's numeric `id`, which you then use with `GET /instagram/post/{id}/comments`.

</details>

<details>

<summary>Are reels returned with a different schema than posts?</summary>

No. Reels use the same [Post object](#post-object) schema as regular posts and tagged posts; the `type` field is set to `reel` (or `video`) to distinguish them.

</details>

<details>

<summary>Why don't stories support pagination?</summary>

Instagram stories expire 24 hours after posting, so the active set is always small enough to return in a single call. There is no `cursor` or `maxItems` parameter on the stories endpoint.

</details>

<details>

<summary>What's the credit cost per request?</summary>

It's dynamic — every response includes `pricing.creditsCharged` with the exact amount for that call. There's no fixed published rate per endpoint or per result.

</details>

## Next Steps

* [**API Integration Guide**](broken://pages/68e75ab1a73d266350bd68301be87ff97cad563e) — integrate the Pullbay API into your backend application.
* [**n8n Integration Guide**](broken://pages/4308164455b56bbc694cf74c9768ea689c2fe719) — set up automated workflows using n8n with no coding required.
* [**Services Overview**](broken://pages/0d55376cbcca70097a683a71a593d3077825dbf1) — explore all available Pullbay services.


# Reddit

The Pullbay Reddit API provides programmatic access to Reddit posts, comments, users, and subreddits. Search posts by keyword, pull a post's comment tree (and replies to any comment), look up a user's profile and their posts/comments, and look up a subreddit's profile and its posts — all without a Reddit account or OAuth app of your own.

**Base URL:** `https://api.pullbay.com`

## Overview

The Reddit service lets you:

* Search Reddit posts by keyword
* Look up a single post by its ID
* Fetch the comment tree on a post, and replies to any individual comment
* Look up a Reddit user's profile, plus the posts and comments they've authored
* Look up a subreddit's profile and the posts submitted to it

All endpoints are read-only `GET` requests against public Reddit data.

## Common Use Cases

**Topic Monitoring** — Use Search Posts on a schedule to track new discussion of a keyword, brand, or product across all of Reddit.

**Community Research** — Pull a subreddit's profile and recent posts to understand its size, rules, and the kind of content that performs well there.

**Sentiment & Discussion Analysis** — Fetch a post's comment tree (and drill into replies) to mine opinions, objections, and recurring questions around a topic.

**Influencer / Contributor Tracking** — Use Get User Posts and Get User Comments to see everything a specific Redditor has published, useful for vetting contributors or tracking a known voice in a community.

**Competitive & Market Intelligence** — Combine Search Posts with Get Subreddit Posts to see how a topic is discussed both broadly and within a specific community.

## Endpoints

| Endpoint                                             | Description                          | Pagination        |
| ---------------------------------------------------- | ------------------------------------ | ----------------- |
| `GET /reddit/search/post`                            | Search Reddit posts by keyword       | Cursor / maxItems |
| `GET /reddit/post/{id}`                              | Get a single post by ID              | —                 |
| `GET /reddit/post/{id}/comments`                     | Get the top-level comments on a post | —                 |
| `GET /reddit/post/{id}/comments/{commentId}/replies` | Get replies to a comment             | —                 |
| `GET /reddit/user/{username}`                        | Get a user profile by username       | —                 |
| `GET /reddit/user/{username}/posts`                  | Get posts submitted by a user        | Cursor / maxItems |
| `GET /reddit/user/{username}/comments`               | Get comments authored by a user      | Cursor / maxItems |
| `GET /reddit/subreddit/{name}`                       | Get a subreddit profile              | —                 |
| `GET /reddit/subreddit/{name}/posts`                 | Get posts from a subreddit           | Cursor / maxItems |

***

### Search Posts

Search Reddit posts by keyword.

**Request**

```bash
GET /reddit/search/post
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Default | Description                                                                                      |
| ---------- | ------- | -------- | ------- | ------------------------------------------------------------------------------------------------ |
| `keyword`  | string  | Yes      | —       | Search term, 1–100 characters                                                                    |
| `sort`     | string  | No       | —       | `relevance`, `hot`, `top`, `new`, or `comments`                                                  |
| `maxItems` | integer | No       | —       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`                              |
| `cursor`   | string  | No       | —       | Pagination cursor from a previous response, 1–100 characters. Mutually exclusive with `maxItems` |

**Example**

```bash
curl -G "https://api.pullbay.com/reddit/search/post" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d keyword="javascript" \
  -d sort=new
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "post",
      "id": "1qclvp3",
      "title": "Is learning JavaScript still a good decision in 2026?",
      "text": "",
      "textHTML": "",
      "url": "/r/learnprogramming/comments/1qclvp3/is_learning_javascript_still_a_good_decision_in/",
      "created": 1768393294,
      "editedAt": false,
      "score": 11,
      "upvotes": 11,
      "downvotes": 0,
      "upvoteRatio": 0.68,
      "numComments": 51,
      "viewCount": null,
      "goldCount": 0,
      "numCrossposts": 0,
      "isLocked": false,
      "isStickied": false,
      "isArchived": false,
      "isOriginalContent": false,
      "isCrosspostable": false,
      "isNSFW": false,
      "isSpoiler": false,
      "isMediaOnly": false,
      "isSponsored": false,
      "isMeta": false,
      "isGildable": false,
      "isScoreHidden": false,
      "contestMode": false,
      "hidden": false,
      "sendReplies": true,
      "author": {
        "id": "t2_1w72",
        "username": "spez",
        "isBlocked": false,
        "isDeleted": false
      },
      "subreddit": {
        "id": "t5_2fwo",
        "name": "learnprogramming",
        "prefixedName": "r/learnprogramming",
        "type": "public"
      },
      "flair": null,
      "thumbnail": {},
      "preview": {},
      "awards": [],
      "flairs": [],
      "gallery": [],
      "crosspostParents": []
    }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                                                       |
| ------------------------ | ------ | ------------------------------------------------------------------------------------------------- |
| `data`                   | array  | Array of [Post objects](#post-object)                                                             |
| `pagination`             | object | Pagination info — see [Sort Values Reference](#sort-values-reference) note below for cursor usage |
| `pricing.creditsCharged` | number | Credits charged for this request                                                                  |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Get Post

Returns a single Reddit post by its ID.

**Request**

```bash
GET /reddit/post/{id}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type          | Required | Default | Description                                                     |
| --------- | ------------- | -------- | ------- | --------------------------------------------------------------- |
| `id`      | string (path) | Yes      | —       | Post ID, 1–32 characters. Substitute directly into the URL path |

**Example**

```bash
curl -G "https://api.pullbay.com/reddit/post/1qclvp3" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "post",
      "id": "1qclvp3",
      "title": "Is learning JavaScript still a good decision in 2026?",
      "text": "",
      "textHTML": "",
      "url": "/r/learnprogramming/comments/1qclvp3/is_learning_javascript_still_a_good_decision_in/",
      "created": 1768393294,
      "editedAt": false,
      "score": 11,
      "upvotes": 11,
      "downvotes": 0,
      "upvoteRatio": 0.68,
      "numComments": 51,
      "viewCount": null,
      "goldCount": 0,
      "numCrossposts": 0,
      "isLocked": false,
      "isStickied": false,
      "isArchived": false,
      "isOriginalContent": false,
      "isCrosspostable": false,
      "isNSFW": false,
      "isSpoiler": false,
      "isMediaOnly": false,
      "isSponsored": false,
      "isMeta": false,
      "isGildable": false,
      "isScoreHidden": false,
      "contestMode": false,
      "hidden": false,
      "sendReplies": true,
      "author": {
        "id": "t2_1w72",
        "username": "spez",
        "isBlocked": false,
        "isDeleted": false
      },
      "subreddit": {
        "id": "t5_2fwo",
        "name": "learnprogramming",
        "prefixedName": "r/learnprogramming",
        "type": "public"
      },
      "flair": null,
      "thumbnail": {},
      "preview": {},
      "awards": [],
      "flairs": [],
      "gallery": [],
      "crosspostParents": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                           |
| ------------------------ | ------ | ----------------------------------------------------- |
| `data`                   | array  | Array containing a single [Post object](#post-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                      |

Note: this endpoint's response has no `pagination` field and accepts no `sort` parameter.

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get Post Comments

Returns the top-level comment tree for a Reddit post.

**Request**

```bash
GET /reddit/post/{id}/comments
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type          | Required | Default | Description                                                     |
| --------- | ------------- | -------- | ------- | --------------------------------------------------------------- |
| `id`      | string (path) | Yes      | —       | Post ID, 1–32 characters. Substitute directly into the URL path |
| `sort`    | string        | No       | —       | `best`, `top`, `new`, `controversial`, `old`, or `qa`           |

**Example**

```bash
curl -G "https://api.pullbay.com/reddit/post/1t0jood/comments" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d sort=top
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "comment",
      "id": "ojb40kw",
      "postId": "t3_1t0jood",
      "parentId": "t3_1t0jood",
      "body": "That was Rich Greenfield from LightShed.",
      "bodyHTML": "",
      "url": "/r/redditstock/comments/1t0jood/.../ojb40kw/",
      "createdAt": 1777638722,
      "editedAt": false,
      "score": 24,
      "upvotes": 24,
      "downvotes": 0,
      "goldCount": 0,
      "totalAwardsReceived": 0,
      "controversiality": 0,
      "isLocked": false,
      "isStickied": false,
      "isArchived": false,
      "isSaved": false,
      "isScoreHidden": false,
      "isCollapsed": false,
      "noFollow": false,
      "sendReplies": true,
      "hasReplies": false,
      "author": {
        "id": "t2_1w72",
        "username": "spez",
        "isBlocked": false,
        "isDeleted": false
      },
      "subreddit": {
        "id": "t5_2fwo",
        "name": "redditstock",
        "prefixedName": "r/redditstock",
        "type": "public"
      },
      "awards": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                 |
| ------------------------ | ------ | ------------------------------------------- |
| `data`                   | array  | Array of [Comment objects](#comment-object) |
| `pricing.creditsCharged` | number | Credits charged for this request            |

Note: despite returning an array of comments, this endpoint's response has **no** `pagination` field — the full top-level comment tree is returned in one call.

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get Comment Replies

Returns replies to a specific comment on a Reddit post.

**Request**

```bash
GET /reddit/post/{id}/comments/{commentId}/replies
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter   | Type          | Required | Default | Description                                                        |
| ----------- | ------------- | -------- | ------- | ------------------------------------------------------------------ |
| `id`        | string (path) | Yes      | —       | Post ID, 1–32 characters. Substitute directly into the URL path    |
| `commentId` | string (path) | Yes      | —       | Comment ID, 1–32 characters. Substitute directly into the URL path |
| `sort`      | string        | No       | —       | `best`, `top`, `new`, `controversial`, `old`, or `qa`              |

**Example**

```bash
curl -G "https://api.pullbay.com/reddit/post/1t0jood/comments/ojb40kw/replies" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d sort=best
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "comment",
      "id": "ojb4abc",
      "postId": "t3_1t0jood",
      "parentId": "t1_ojb40kw",
      "body": "Source on that?",
      "bodyHTML": "",
      "url": "/r/redditstock/comments/1t0jood/.../ojb4abc/",
      "createdAt": 1777639022,
      "editedAt": false,
      "score": 4,
      "upvotes": 4,
      "downvotes": 0,
      "goldCount": 0,
      "totalAwardsReceived": 0,
      "controversiality": 0,
      "isLocked": false,
      "isStickied": false,
      "isArchived": false,
      "isSaved": false,
      "isScoreHidden": false,
      "isCollapsed": false,
      "noFollow": false,
      "sendReplies": true,
      "hasReplies": false,
      "author": {
        "id": "t2_9f21",
        "username": "throwaway_invest",
        "isBlocked": false,
        "isDeleted": false
      },
      "subreddit": {
        "id": "t5_2fwo",
        "name": "redditstock",
        "prefixedName": "r/redditstock",
        "type": "public"
      },
      "awards": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                 |
| ------------------------ | ------ | ------------------------------------------- |
| `data`                   | array  | Array of [Comment objects](#comment-object) |
| `pricing.creditsCharged` | number | Credits charged for this request            |

Note: this endpoint's response also has **no** `pagination` field.

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get User

Returns a Reddit user profile by username (without `u/`).

**Request**

```bash
GET /reddit/user/{username}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type          | Required | Default | Description                                                                                      |
| ---------- | ------------- | -------- | ------- | ------------------------------------------------------------------------------------------------ |
| `username` | string (path) | Yes      | —       | Reddit username without the `u/` prefix, 1–100 characters. Substitute directly into the URL path |

**Example**

```bash
curl -G "https://api.pullbay.com/reddit/user/spez" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "user",
      "id": "1w72",
      "name": "spez",
      "title": "spez",
      "description": "Reddit CEO",
      "createdAt": 1118030400,
      "iconImage": "https://styles.redditmedia.com/t5_3k30p/styles/profileIcon_uj015iwx9s7g1.png",
      "snoovatarImage": "",
      "bannerImage": "https://b.thumbs.redditmedia.com/KWeEpVxXOGLoloMbM0IxGt9EiKPXizpwFgcSeWqtpZM.png",
      "awarderKarma": 0,
      "postKarma": 182496,
      "commentKarma": 755311,
      "totalKarma": 937807,
      "hasVerifiedEmail": true,
      "over18": false
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                           |
| ------------------------ | ------ | ----------------------------------------------------- |
| `data`                   | array  | Array containing a single [User object](#user-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                      |

Note: this endpoint's response has no `pagination` field and accepts no `sort` parameter.

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get User Posts

Returns posts submitted by a Reddit user.

**Request**

```bash
GET /reddit/user/{username}/posts
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type          | Required | Default | Description                                                                                      |
| ---------- | ------------- | -------- | ------- | ------------------------------------------------------------------------------------------------ |
| `username` | string (path) | Yes      | —       | Reddit username without the `u/` prefix, 1–100 characters. Substitute directly into the URL path |
| `sort`     | string        | No       | —       | `hot`, `top`, or `new`                                                                           |
| `maxItems` | integer       | No       | —       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`                              |
| `cursor`   | string        | No       | —       | Pagination cursor from a previous response, 1–100 characters. Mutually exclusive with `maxItems` |

**Example**

```bash
curl -G "https://api.pullbay.com/reddit/user/spez/posts" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d sort=new \
  -d maxItems=50
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    { "type": "post", "id": "1qclvp3", "title": "Is learning JavaScript still a good decision in 2026?", "...": "..." }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                                                       |
| ------------------------ | ------ | ------------------------------------------------------------------------------------------------- |
| `data`                   | array  | Array of [Post objects](#post-object)                                                             |
| `pagination`             | object | Pagination info — see [Sort Values Reference](#sort-values-reference) note below for cursor usage |
| `pricing.creditsCharged` | number | Credits charged for this request                                                                  |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get User Comments

Returns comments authored by a Reddit user.

**Request**

```bash
GET /reddit/user/{username}/comments
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type          | Required | Default | Description                                                                                      |
| ---------- | ------------- | -------- | ------- | ------------------------------------------------------------------------------------------------ |
| `username` | string (path) | Yes      | —       | Reddit username without the `u/` prefix, 1–100 characters. Substitute directly into the URL path |
| `sort`     | string        | No       | —       | `hot`, `top`, or `new`                                                                           |
| `maxItems` | integer       | No       | —       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`                              |
| `cursor`   | string        | No       | —       | Pagination cursor from a previous response, 1–100 characters. Mutually exclusive with `maxItems` |

**Example**

```bash
curl -G "https://api.pullbay.com/reddit/user/spez/comments" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d sort=top \
  -d cursor=eyJhZnRlciI6InQxX29qYjQwa3cifQ
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    { "type": "comment", "id": "ojb40kw", "body": "That was Rich Greenfield from LightShed.", "...": "..." }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                                                       |
| ------------------------ | ------ | ------------------------------------------------------------------------------------------------- |
| `data`                   | array  | Array of [Comment objects](#comment-object)                                                       |
| `pagination`             | object | Pagination info — see [Sort Values Reference](#sort-values-reference) note below for cursor usage |
| `pricing.creditsCharged` | number | Credits charged for this request                                                                  |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get Subreddit

Returns a subreddit profile by its name (without `r/`).

**Request**

```bash
GET /reddit/subreddit/{name}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type          | Required | Default | Description                                                                                     |
| --------- | ------------- | -------- | ------- | ----------------------------------------------------------------------------------------------- |
| `name`    | string (path) | Yes      | —       | Subreddit name without the `r/` prefix, 1–100 characters. Substitute directly into the URL path |

**Example**

```bash
curl -G "https://api.pullbay.com/reddit/subreddit/programming" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "subreddit",
      "id": "2fwo",
      "name": "programming",
      "prefixedName": "r/programming",
      "title": "programming",
      "subType": "public",
      "description": "/r/programming is a reddit for discussion and news about computer programming.",
      "descriptionHtml": "",
      "publicDescription": "Computer Programming",
      "publicDescriptionHtml": "",
      "submitText": "",
      "submitTextHtml": null,
      "submissionType": "link",
      "advertiserCategory": "Technology",
      "language": "en",
      "url": "/r/programming/",
      "headerTitle": "",
      "iconImage": "",
      "iconSize": null,
      "bannerImage": "",
      "bannerBackgroundImage": "",
      "bannerSize": null,
      "headerImage": "https://b.thumbs.redditmedia.com/2rTE46grzsr-Ll3Q.png",
      "headerSize": [120, 40],
      "primaryColor": "#002b36",
      "keyColor": "",
      "createdAt": 1141150769,
      "subscribers": 6873180,
      "commentScoreHideMins": 0,
      "notificationLevel": null,
      "suggestedCommentSort": null,
      "userFlairEnabled": true,
      "linkFlairEnabled": true,
      "wikiEnabled": true,
      "emojisEnabled": false,
      "spoilersEnabled": true,
      "allowImages": false,
      "allowVideos": false,
      "allowGalleries": false,
      "allowPolls": false,
      "allowPredictions": false,
      "allowTalks": false,
      "allowDiscovery": true,
      "isCrosspostable": false,
      "publicTraffic": false,
      "showMediaPreview": true,
      "showMedia": false,
      "collapseDeletedComments": false,
      "shouldArchivePosts": false,
      "canAssignUserFlair": false,
      "canAssignLinkFlair": false,
      "restrictCommenting": false,
      "restrictPosting": true,
      "communityReviewed": true,
      "flairPosition": "right",
      "linkFlairPosition": "right"
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                     |
| ------------------------ | ------ | --------------------------------------------------------------- |
| `data`                   | array  | Array containing a single [Subreddit object](#subreddit-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                                |

Note: this endpoint's response has no `pagination` field and accepts no `sort` parameter.

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get Subreddit Posts

Returns posts from a subreddit.

**Request**

```bash
GET /reddit/subreddit/{name}/posts
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type          | Required | Default | Description                                                                                      |
| ---------- | ------------- | -------- | ------- | ------------------------------------------------------------------------------------------------ |
| `name`     | string (path) | Yes      | —       | Subreddit name without the `r/` prefix, 1–100 characters. Substitute directly into the URL path  |
| `sort`     | string        | No       | —       | `hot`, `top`, or `new`                                                                           |
| `maxItems` | integer       | No       | —       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`                              |
| `cursor`   | string        | No       | —       | Pagination cursor from a previous response, 1–100 characters. Mutually exclusive with `maxItems` |

**Example**

```bash
curl -G "https://api.pullbay.com/reddit/subreddit/programming/posts" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d sort=hot
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    { "type": "post", "id": "1qclvp3", "title": "Is learning JavaScript still a good decision in 2026?", "...": "..." }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                                                       |
| ------------------------ | ------ | ------------------------------------------------------------------------------------------------- |
| `data`                   | array  | Array of [Post objects](#post-object)                                                             |
| `pagination`             | object | Pagination info — see [Sort Values Reference](#sort-values-reference) note below for cursor usage |
| `pricing.creditsCharged` | number | Credits charged for this request                                                                  |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                     |
| ------ | ------------------------------------------- |
| `400`  | Bad request — missing/invalid parameters    |
| `402`  | Insufficient credits                        |
| `404`  | Post, comment, user, or subreddit not found |

## Object Schema

### Post object

| Field                                                                                                                         | Type                   | Description                                                   |
| ----------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ------------------------------------------------------------- |
| `type`                                                                                                                        | string                 | Always `post`                                                 |
| `id`                                                                                                                          | string                 | Post ID                                                       |
| `title`                                                                                                                       | string                 | Post title                                                    |
| `text`                                                                                                                        | string                 | Self-text body (empty for link posts)                         |
| `textHTML`                                                                                                                    | string                 | Rendered HTML of the self-text body                           |
| `url`                                                                                                                         | string                 | Relative permalink to the post                                |
| `created`                                                                                                                     | integer (unix seconds) | Submission timestamp                                          |
| `editedAt`                                                                                                                    | integer or boolean     | Edit timestamp, or `false` if never edited                    |
| `score`                                                                                                                       | integer                | Net score (upvotes minus downvotes)                           |
| `upvotes` / `downvotes`                                                                                                       | integer                | Vote counts                                                   |
| `upvoteRatio`                                                                                                                 | number                 | Fraction of votes that are upvotes                            |
| `numComments`                                                                                                                 | integer                | Total comment count                                           |
| `viewCount`                                                                                                                   | integer, nullable      | View count, when available                                    |
| `goldCount`                                                                                                                   | integer                | Number of Reddit Gold awards                                  |
| `numCrossposts`                                                                                                               | integer                | Number of crossposts of this post                             |
| `isLocked` / `isStickied` / `isArchived`                                                                                      | boolean                | Moderation/state flags                                        |
| `isOriginalContent` / `isCrosspostable`                                                                                       | boolean                | Content flags                                                 |
| `isNSFW` / `isSpoiler` / `isMediaOnly` / `isSponsored` / `isMeta` / `isGildable` / `isScoreHidden` / `contestMode` / `hidden` | boolean                | Additional content/state flags                                |
| `sendReplies`                                                                                                                 | boolean                | Whether the author allows reply notifications                 |
| `author`                                                                                                                      | object                 | See [Author object](#author-object)                           |
| `subreddit`                                                                                                                   | object                 | See [Subreddit Reference object](#subreddit-reference-object) |
| `flair`                                                                                                                       | object, nullable       | Link flair, when set                                          |
| `thumbnail`                                                                                                                   | object                 | Thumbnail image metadata                                      |
| `preview`                                                                                                                     | object                 | Preview image/media metadata                                  |
| `awards`                                                                                                                      | array                  | Awards given to the post                                      |
| `flairs`                                                                                                                      | array                  | Available flair templates                                     |
| `gallery`                                                                                                                     | array                  | Gallery media items, for gallery posts                        |
| `crosspostParents`                                                                                                            | array                  | Parent post(s), for crossposts                                |

### Comment object

| Field                                                                                  | Type                   | Description                                                   |
| -------------------------------------------------------------------------------------- | ---------------------- | ------------------------------------------------------------- |
| `type`                                                                                 | string                 | Always `comment`                                              |
| `id`                                                                                   | string                 | Comment ID                                                    |
| `postId`                                                                               | string                 | Fullname of the post the comment belongs to                   |
| `parentId`                                                                             | string                 | Fullname of the parent (post or comment)                      |
| `body`                                                                                 | string                 | Comment text                                                  |
| `bodyHTML`                                                                             | string                 | Rendered HTML of the comment text                             |
| `url`                                                                                  | string                 | Relative permalink to the comment                             |
| `createdAt`                                                                            | integer (unix seconds) | When the comment was posted                                   |
| `editedAt`                                                                             | integer or boolean     | Edit timestamp, or `false` if never edited                    |
| `score`                                                                                | integer                | Net score (upvotes minus downvotes)                           |
| `upvotes` / `downvotes`                                                                | integer                | Vote counts                                                   |
| `goldCount`                                                                            | integer                | Number of Reddit Gold awards                                  |
| `totalAwardsReceived`                                                                  | integer                | Total awards received                                         |
| `controversiality`                                                                     | integer                | Reddit's controversiality flag (0 or 1)                       |
| `isLocked` / `isStickied` / `isArchived` / `isSaved` / `isScoreHidden` / `isCollapsed` | boolean                | Moderation/state flags                                        |
| `noFollow`                                                                             | boolean                | Whether links in the comment are `nofollow`                   |
| `sendReplies`                                                                          | boolean                | Whether the author allows reply notifications                 |
| `hasReplies`                                                                           | boolean                | Whether this comment has reply comments                       |
| `author`                                                                               | object                 | See [Author object](#author-object)                           |
| `subreddit`                                                                            | object                 | See [Subreddit Reference object](#subreddit-reference-object) |
| `awards`                                                                               | array                  | Awards given to the comment                                   |

### User object

| Field              | Type                   | Description                              |
| ------------------ | ---------------------- | ---------------------------------------- |
| `type`             | string                 | Always `user`                            |
| `id`               | string                 | User ID (without `t2_` prefix)           |
| `name`             | string                 | Username                                 |
| `title`            | string                 | Profile title (often same as username)   |
| `description`      | string                 | Profile bio/description                  |
| `createdAt`        | integer (unix seconds) | Account creation timestamp               |
| `iconImage`        | string                 | Avatar image URL                         |
| `snoovatarImage`   | string                 | Snoovatar image URL, if set              |
| `bannerImage`      | string                 | Profile banner image URL                 |
| `awarderKarma`     | integer                | Karma earned from giving awards          |
| `postKarma`        | integer                | Karma earned from posts                  |
| `commentKarma`     | integer                | Karma earned from comments               |
| `totalKarma`       | integer                | Combined karma total                     |
| `hasVerifiedEmail` | boolean                | Whether the account has a verified email |
| `over18`           | boolean                | Whether the account is marked NSFW       |

### Subreddit object

| Field                                                                                               | Type                     | Description                                           |
| --------------------------------------------------------------------------------------------------- | ------------------------ | ----------------------------------------------------- |
| `type`                                                                                              | string                   | Always `subreddit`                                    |
| `id`                                                                                                | string                   | Subreddit ID (without `t5_` prefix)                   |
| `name`                                                                                              | string                   | Subreddit name (without `r/`)                         |
| `prefixedName`                                                                                      | string                   | Subreddit name with `r/` prefix                       |
| `title`                                                                                             | string                   | Subreddit title                                       |
| `subType`                                                                                           | string                   | Subreddit visibility type (e.g. `public`)             |
| `description` / `descriptionHtml`                                                                   | string                   | Sidebar description, plain and HTML                   |
| `publicDescription` / `publicDescriptionHtml`                                                       | string                   | Short public description, plain and HTML              |
| `submitText` / `submitTextHtml`                                                                     | string, nullable         | Text shown on the submit page                         |
| `submissionType`                                                                                    | string                   | Allowed submission type (e.g. `link`, `self`, `any`)  |
| `advertiserCategory`                                                                                | string                   | Ad category classification                            |
| `language`                                                                                          | string                   | Subreddit's primary language code                     |
| `url`                                                                                               | string                   | Relative URL to the subreddit                         |
| `headerTitle`                                                                                       | string                   | Header link title                                     |
| `iconImage` / `iconSize`                                                                            | string / array, nullable | Icon image URL and dimensions                         |
| `bannerImage` / `bannerBackgroundImage` / `bannerSize`                                              | string / array, nullable | Banner images and dimensions                          |
| `headerImage` / `headerSize`                                                                        | string / array           | Header image URL and dimensions                       |
| `primaryColor` / `keyColor`                                                                         | string                   | Theme colors                                          |
| `createdAt`                                                                                         | integer (unix seconds)   | Subreddit creation timestamp                          |
| `subscribers`                                                                                       | integer                  | Subscriber count                                      |
| `commentScoreHideMins`                                                                              | integer                  | Minutes comment scores are hidden after posting       |
| `notificationLevel` / `suggestedCommentSort`                                                        | string, nullable         | User-specific notification setting and suggested sort |
| `userFlairEnabled` / `linkFlairEnabled`                                                             | boolean                  | Flair feature flags                                   |
| `wikiEnabled` / `emojisEnabled` / `spoilersEnabled`                                                 | boolean                  | Feature flags                                         |
| `allowImages` / `allowVideos` / `allowGalleries` / `allowPolls` / `allowPredictions` / `allowTalks` | boolean                  | Allowed content type flags                            |
| `allowDiscovery` / `isCrosspostable` / `publicTraffic`                                              | boolean                  | Discovery and traffic flags                           |
| `showMediaPreview` / `showMedia`                                                                    | boolean                  | Media display settings                                |
| `collapseDeletedComments` / `shouldArchivePosts`                                                    | boolean                  | Moderation settings                                   |
| `canAssignUserFlair` / `canAssignLinkFlair`                                                         | boolean                  | Flair assignment permissions                          |
| `restrictCommenting` / `restrictPosting`                                                            | boolean                  | Posting/commenting restrictions                       |
| `communityReviewed`                                                                                 | boolean                  | Whether the subreddit has passed community review     |
| `flairPosition` / `linkFlairPosition`                                                               | string                   | Flair display position (e.g. `right`)                 |

### Author object

Embedded in `Post` and `Comment` objects as the `author` field.

| Field       | Type    | Description                                             |
| ----------- | ------- | ------------------------------------------------------- |
| `id`        | string  | Author's user ID (fullname, e.g. `t2_1w72`)             |
| `username`  | string  | Author's username                                       |
| `isBlocked` | boolean | Whether the author is blocked by the requesting context |
| `isDeleted` | boolean | Whether the author's account has been deleted           |

### Subreddit Reference object

Embedded in `Post` and `Comment` objects as the `subreddit` field.

| Field          | Type   | Description                               |
| -------------- | ------ | ----------------------------------------- |
| `id`           | string | Subreddit ID (fullname, e.g. `t5_2fwo`)   |
| `name`         | string | Subreddit name (without `r/`)             |
| `prefixedName` | string | Subreddit name with `r/` prefix           |
| `type`         | string | Subreddit visibility type (e.g. `public`) |

## Finding Post and Comment IDs

* **Post ID**: the alphanumeric segment in a Reddit URL, e.g. `https://reddit.com/r/learnprogramming/comments/1qclvp3/...` → `1qclvp3`.
* **Comment ID**: the alphanumeric segment after the post ID in a comment permalink, e.g. `.../comments/1t0jood/some_title/ojb40kw/` → `ojb40kw`.
* **Username**: the part after `u/` in a profile URL, e.g. `https://reddit.com/u/spez` → `spez`. Pass it without the `u/` prefix.
* **Subreddit name**: the part after `r/` in a subreddit URL, e.g. `https://reddit.com/r/programming` → `programming`. Pass it without the `r/` prefix.
* The Search Posts endpoint is the fastest way to discover post IDs for a topic without already knowing a permalink.

## Sort Values Reference

The `sort` parameter is **not** a single global enum — it has three different, non-overlapping value sets depending on which endpoint you're calling. Using a value from the wrong scope (e.g. passing `qa` to Search Posts, or `relevance` to Get Subreddit Posts) is invalid.

**Scope A — Search Posts only**

Endpoint: `GET /reddit/search/post`

Values: `relevance`, `hot`, `top`, `new`, `comments`

This scope is unique to the search endpoint and is not valid anywhere else.

**Scope B — Comment-fetching endpoints**

Endpoints: `GET /reddit/post/{id}/comments` and `GET /reddit/post/{id}/comments/{commentId}/replies`

Values: `best`, `top`, `new`, `controversial`, `old`, `qa`

This scope applies only to the two endpoints that return comments. It is not valid for post-search or for the "list by user/subreddit" endpoints below.

**Scope C — "List by user/subreddit" endpoints**

Endpoints: `GET /reddit/user/{username}/posts`, `GET /reddit/user/{username}/comments`, `GET /reddit/subreddit/{name}/posts`

Values: `hot`, `top`, `new`

This scope is shared by the three endpoints that list a user's or subreddit's posts/comments. Note that `top` and `new` happen to overlap in name with values from Scope A and Scope B, but each endpoint only accepts the values listed in its own scope above — overlap in spelling does not mean the scopes are interchangeable.

| Endpoint                                             | Sort scope | Accepted values                                    |
| ---------------------------------------------------- | ---------- | -------------------------------------------------- |
| `GET /reddit/search/post`                            | A          | `relevance`, `hot`, `top`, `new`, `comments`       |
| `GET /reddit/post/{id}/comments`                     | B          | `best`, `top`, `new`, `controversial`, `old`, `qa` |
| `GET /reddit/post/{id}/comments/{commentId}/replies` | B          | `best`, `top`, `new`, `controversial`, `old`, `qa` |
| `GET /reddit/user/{username}/posts`                  | C          | `hot`, `top`, `new`                                |
| `GET /reddit/user/{username}/comments`               | C          | `hot`, `top`, `new`                                |
| `GET /reddit/subreddit/{name}/posts`                 | C          | `hot`, `top`, `new`                                |

The remaining three endpoints (`GET /reddit/post/{id}`, `GET /reddit/user/{username}`, `GET /reddit/subreddit/{name}`) take no `sort` parameter at all — each returns a single resource, not a list.

## Python Example

This example searches for posts matching a keyword and pages through results using `cursor`, stopping when no further cursor is returned:

```python
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.pullbay.com"

def search_all_posts(keyword, sort="new", max_pages=10):
    all_posts = []
    cursor = None
    page = 0
    while page < max_pages:
        params = {"keyword": keyword, "sort": sort}
        if cursor:
            params["cursor"] = cursor
        resp = requests.get(
            f"{BASE_URL}/reddit/search/post",
            headers={"Authorization": f"Bearer {API_KEY}"},
            params=params,
        )
        resp.raise_for_status()
        body = resp.json()
        all_posts.extend(body["data"])
        cursor = body.get("pagination", {}).get("cursor")
        if not cursor:
            break
        page += 1
    return all_posts

posts = search_all_posts("javascript")
print(f"Fetched {len(posts)} posts")
```

Each request is billed independently, with the charged amount reported in that response's `pricing.creditsCharged`.

## FAQ

<details>

<summary>Why does <code>sort</code> accept different values on different endpoints?</summary>

Reddit itself exposes different sort orders for search results, comment threads, and post/comment listings — Pullbay passes these through as-is rather than normalizing them into one shared list. See [Sort Values Reference](#sort-values-reference) for the exact values per endpoint.

</details>

<details>

<summary>Can I paginate the comment or comment-replies endpoints?</summary>

No. `GET /reddit/post/{id}/comments` and `GET /reddit/post/{id}/comments/{commentId}/replies` return their full result in a single response with no `pagination` field — there's nothing to page through.

</details>

<details>

<summary>How do I get more than 200 results from a list endpoint?</summary>

`maxItems` is capped at 200 per call. For more, use `cursor`-based pagination across multiple requests instead of `maxItems`.

</details>

<details>

<summary>Can I use <code>cursor</code> and <code>maxItems</code> together?</summary>

No, they're mutually exclusive on every endpoint that supports them. Use `cursor` to page manually or `maxItems` for a single bulk pull, not both in the same request.

</details>

<details>

<summary>What's the credit cost per request?</summary>

It's dynamic — every response includes `pricing.creditsCharged` with the exact amount for that call. There's no fixed published rate per endpoint.

</details>

<details>

<summary>Do I need a Reddit account or API credentials from Reddit?</summary>

No. All endpoints return public Reddit data through Pullbay — you only need your Pullbay API key.

</details>

## Next Steps

* [**API Integration Guide**](broken://pages/68e75ab1a73d266350bd68301be87ff97cad563e) — integrate the Pullbay API into your backend application.
* [**n8n Integration Guide**](broken://pages/4308164455b56bbc694cf74c9768ea689c2fe719) — set up automated workflows using n8n with no coding required.
* [**Services Overview**](broken://pages/0d55376cbcca70097a683a71a593d3077825dbf1) — explore all available Pullbay services.


# TikTok

The Pullbay TikTok API provides programmatic access to public TikTok data — user profiles, posts, comments, replies, hashtags, and keyword search. Integrate TikTok monitoring into your applications, dashboards, and analytics pipelines without scraping or maintaining your own TikTok session.

**Base URL:** `https://api.pullbay.com`

## Overview

The TikTok service lets you:

* Look up a user profile by handle
* Fetch the posts published by a given user
* Fetch a single post by its numeric ID or by its full URL
* Fetch comments on a post, and replies to a specific comment
* Search posts by keyword
* Look up a hashtag profile by name

All endpoints are read-only `GET` requests against public TikTok data — you don't need a TikTok account of your own.

## Common Use Cases

**Trend Research** — Use Search Posts with a keyword to see what's circulating right now, then pull engagement metrics (`views`, `likes`, `shares`) to gauge how a trend is performing.

**Hashtag & Campaign Tracking** — Use Get Hashtag by Name to monitor total view counts on a branded or campaign hashtag over time.

**Creator & Competitor Analysis** — Use Get User by Handle and Get User's Posts to track a creator's or competitor's posting cadence, follower growth, and per-post performance.

**Conversation & Sentiment Monitoring** — Use Get Post Comments and Get Comment Replies to pull the conversation under a post and feed comment text into your own sentiment-analysis or moderation pipeline.

**Content Archiving & Reporting** — Resolve posts by ID or by URL to attach canonical TikTok metadata (caption, stats, channel) to internal reports or dashboards.

## Endpoints

| Endpoint                                             | Description                   | Pagination        |
| ---------------------------------------------------- | ----------------------------- | ----------------- |
| `GET /tiktok/user/handle/{username}`                 | Get a user by handle          | —                 |
| `GET /tiktok/user/{id}/posts`                        | Get posts published by a user | Cursor / maxItems |
| `GET /tiktok/post/{id}`                              | Get a post by ID              | —                 |
| `GET /tiktok/post/{id}/comments`                     | Get comments on a post        | Cursor / maxItems |
| `GET /tiktok/post/{id}/comments/{commentId}/replies` | Get replies to a comment      | Cursor / maxItems |
| `GET /tiktok/post/search`                            | Search TikTok posts           | Cursor / maxItems |
| `GET /tiktok/post`                                   | Get a post by URL             | —                 |
| `GET /tiktok/hashtag/handle/{name}`                  | Get a hashtag by name         | —                 |

***

### Get User by Handle

Returns a TikTok user profile by handle (without `@`).

**Request**

```bash
GET /tiktok/user/handle/{username}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type   | Required | Default | Description                                                 |
| ---------- | ------ | -------- | ------- | ----------------------------------------------------------- |
| `username` | string | Yes      | —       | TikTok handle without `@`, 1–100 characters (e.g. `tiktok`) |

**Example**

```bash
curl -G "https://api.pullbay.com/tiktok/user/handle/tiktok" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "107955",
      "username": "tiktok",
      "nickname": "TikTok",
      "url": "https://www.tiktok.com/@tiktok",
      "bio": "One TikTok can make a big impact",
      "bioUrl": "https://linktr.ee/tiktok",
      "avatar": "https://p16-common-sign.tiktokcdn.com/tos-maliva-avt-0068/avatar.jpg",
      "verified": true,
      "profileCategory": "",
      "followers": 89000000,
      "following": 7,
      "likes": 1300000000,
      "videos": 1100
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                      |
| ------------------------ | ------ | ---------------------------------------------------------------- |
| `data`                   | array  | Array containing a single [User object](#user-object-tiktokuser) |
| `pricing.creditsCharged` | number | Credits charged for this request                                 |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Get User's Posts

Returns posts published by a TikTok user. Use `cursor` to paginate, or `maxItems` for a one-shot bulk pull — the two are mutually exclusive.

**Request**

```bash
GET /tiktok/user/{id}/posts
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required   | Default | Description                                                                                                             |
| ---------- | ------- | ---------- | ------- | ----------------------------------------------------------------------------------------------------------------------- |
| `id`       | string  | Yes (path) | —       | TikTok user ID, 1–100 characters                                                                                        |
| `region`   | string  | No         | —       | 2–5 character region/country code. The spec does not document a fixed enum for this field — treat it as an open string. |
| `maxItems` | integer | No         | —       | One-shot bulk pull, 1–200 results                                                                                       |
| `cursor`   | string  | No         | —       | Pagination cursor returned by a previous call                                                                           |

**Example**

```bash
curl -G "https://api.pullbay.com/tiktok/user/107955/posts" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d region=US \
  -d maxItems=20
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "7636131611674692894",
      "title": "Check this out!",
      "postPage": "https://www.tiktok.com/@tiktok/video/7636131611674692894",
      "uploadedAt": 1714560000,
      "uploadedAtFormatted": "2024-05-01T00:00:00Z",
      "views": 750000,
      "likes": 12500,
      "comments": 793,
      "shares": 1250,
      "bookmarks": 320,
      "poi": null,
      "channel": {
        "id": "107955",
        "username": "tiktok",
        "name": "TikTok",
        "bio": "One TikTok can make a big impact",
        "avatar": "https://p16-common-sign.tiktokcdn.com/tos-maliva-avt-0068/avatar.jpg"
      },
      "collabInfo": {},
      "video": {},
      "song": {},
      "hashtags": [],
      "subtitleInformation": []
    }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                                          | Type                       | Description                                                     |
| ---------------------------------------------- | -------------------------- | --------------------------------------------------------------- |
| `data`                                         | array                      | Array of [Post objects](#post-object-tiktokpost)                |
| `pagination.cursor` / `pagination.hasNextPage` | string / boolean, nullable | Cursor for the next page and whether more results are available |
| `pricing.creditsCharged`                       | number                     | Credits charged for this request                                |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Get Post by ID

Returns a single TikTok post by its numeric ID.

> **Note:** there are two distinct ways to fetch a single post — by numeric ID (this endpoint, `GET /tiktok/post/{id}`) or by full URL (`GET /tiktok/post`, documented separately below). They are different operations with different parameters; pick the one that matches the data you already have on hand.

**Request**

```bash
GET /tiktok/post/{id}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required   | Default | Description                                                          |
| --------- | ------ | ---------- | ------- | -------------------------------------------------------------------- |
| `id`      | string | Yes (path) | —       | Numeric TikTok post ID, 1–50 characters                              |
| `region`  | string | No         | —       | 2–5 character region/country code. No documented enum — open string. |

**Example**

```bash
curl -G "https://api.pullbay.com/tiktok/post/7636131611674692894" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d region=US
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "7636131611674692894",
      "title": "Check this out!",
      "postPage": "https://www.tiktok.com/@tiktok/video/7636131611674692894",
      "uploadedAt": 1714560000,
      "uploadedAtFormatted": "2024-05-01T00:00:00Z",
      "views": 750000,
      "likes": 12500,
      "comments": 793,
      "shares": 1250,
      "bookmarks": 320,
      "poi": null,
      "channel": {
        "id": "107955",
        "username": "tiktok",
        "name": "TikTok",
        "bio": "One TikTok can make a big impact",
        "avatar": "https://p16-common-sign.tiktokcdn.com/tos-maliva-avt-0068/avatar.jpg"
      },
      "collabInfo": {},
      "video": {},
      "song": {},
      "hashtags": [],
      "subtitleInformation": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                      |
| ------------------------ | ------ | ---------------------------------------------------------------- |
| `data`                   | array  | Array containing a single [Post object](#post-object-tiktokpost) |
| `pricing.creditsCharged` | number | Credits charged for this request                                 |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Get Post Comments

Returns comments on a TikTok post. Use `cursor` to paginate, or `maxItems` for a one-shot bulk pull — mutually exclusive.

**Request**

```bash
GET /tiktok/post/{id}/comments
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required   | Default | Description                                   |
| ---------- | ------- | ---------- | ------- | --------------------------------------------- |
| `id`       | string  | Yes (path) | —       | TikTok post ID, 1–100 characters              |
| `maxItems` | integer | No         | —       | One-shot bulk pull, 1–500 results             |
| `cursor`   | string  | No         | —       | Pagination cursor returned by a previous call |

**Example**

```bash
curl -G "https://api.pullbay.com/tiktok/post/7636131611674692894/comments" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d maxItems=50
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "7636187917325304607",
      "awemeId": "7636131611674692894",
      "text": "Love this!",
      "createdAt": "2026-05-05T19:46:32.000Z",
      "likeCount": 25,
      "replyCount": 3,
      "commentLanguage": "en",
      "isAuthorLiked": false,
      "user": {
        "id": "12345",
        "username": "someuser",
        "nickname": "Some User",
        "avatar": "https://p16-common-sign.tiktokcdn.com/tos-maliva-avt-0068/avatar2.jpg",
        "verified": false
      }
    }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                                          | Type                       | Description                                                     |
| ---------------------------------------------- | -------------------------- | --------------------------------------------------------------- |
| `data`                                         | array                      | Array of [Comment objects](#comment-object-tiktokcomment)       |
| `pagination.cursor` / `pagination.hasNextPage` | string / boolean, nullable | Cursor for the next page and whether more results are available |
| `pricing.creditsCharged`                       | number                     | Credits charged for this request                                |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Get Comment Replies

Returns replies to a specific TikTok comment. Use `cursor` to paginate, or `maxItems` for a one-shot bulk pull — mutually exclusive.

**Request**

```bash
GET /tiktok/post/{id}/comments/{commentId}/replies
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter   | Type    | Required   | Default | Description                                   |
| ----------- | ------- | ---------- | ------- | --------------------------------------------- |
| `id`        | string  | Yes (path) | —       | TikTok post ID, 1–100 characters              |
| `commentId` | string  | Yes (path) | —       | ID of the parent comment, 1–100 characters    |
| `maxItems`  | integer | No         | —       | One-shot bulk pull, 1–500 results             |
| `cursor`    | string  | No         | —       | Pagination cursor returned by a previous call |

**Example**

```bash
curl -G "https://api.pullbay.com/tiktok/post/7636131611674692894/comments/7636187917325304607/replies" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d maxItems=50
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "7636187917325304608",
      "parentId": "7636187917325304607",
      "awemeId": "7636131611674692894",
      "text": "Totally agree!",
      "createdAt": "2026-05-05T20:01:12.000Z",
      "likeCount": 4,
      "commentLanguage": "en",
      "isAuthorLiked": false,
      "user": {
        "id": "67890",
        "username": "anotheruser",
        "nickname": "Another User",
        "avatar": "https://p16-common-sign.tiktokcdn.com/tos-maliva-avt-0068/avatar3.jpg",
        "verified": false
      }
    }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                                          | Type                       | Description                                                     |
| ---------------------------------------------- | -------------------------- | --------------------------------------------------------------- |
| `data`                                         | array                      | Array of [Comment objects](#comment-object-tiktokcomment)       |
| `pagination.cursor` / `pagination.hasNextPage` | string / boolean, nullable | Cursor for the next page and whether more results are available |
| `pricing.creditsCharged`                       | number                     | Credits charged for this request                                |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Search TikTok Posts

Search TikTok posts by keyword. Use `cursor` to paginate, or `maxItems` for a one-shot bulk pull — mutually exclusive.

**Request**

```bash
GET /tiktok/post/search
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter   | Type    | Required | Default | Description                                                                                                                                                                 |
| ----------- | ------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `keyword`   | string  | Yes      | —       | Search term, 1–100 characters                                                                                                                                               |
| `region`    | string  | No       | —       | 2–5 character region/country code. No documented enum — open string.                                                                                                        |
| `dateRange` | string  | No       | —       | A date-range filter string. The spec does not document a fixed set of accepted values for this field — treat it as open/undocumented rather than assuming specific options. |
| `sortType`  | string  | No       | —       | A sort-order string. The spec does not document a fixed set of accepted values for this field — treat it as open/undocumented rather than assuming specific options.        |
| `maxItems`  | integer | No       | —       | One-shot bulk pull, 1–200 results                                                                                                                                           |
| `cursor`    | string  | No       | —       | Pagination cursor returned by a previous call                                                                                                                               |

**Example**

```bash
curl -G "https://api.pullbay.com/tiktok/post/search" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d keyword="cooking hacks" \
  -d region=US \
  -d maxItems=20
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "7636131611674692894",
      "title": "Check this out!",
      "postPage": "https://www.tiktok.com/@tiktok/video/7636131611674692894",
      "uploadedAt": 1714560000,
      "uploadedAtFormatted": "2024-05-01T00:00:00Z",
      "views": 750000,
      "likes": 12500,
      "comments": 793,
      "shares": 1250,
      "bookmarks": 320,
      "poi": null,
      "channel": {
        "id": "107955",
        "username": "tiktok",
        "name": "TikTok",
        "bio": "One TikTok can make a big impact",
        "avatar": "https://p16-common-sign.tiktokcdn.com/tos-maliva-avt-0068/avatar.jpg"
      },
      "collabInfo": {},
      "video": {},
      "song": {},
      "hashtags": [],
      "subtitleInformation": []
    }
  ],
  "pagination": { "page": null, "hasNextPage": null, "cursor": null, "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                                          | Type                       | Description                                                     |
| ---------------------------------------------- | -------------------------- | --------------------------------------------------------------- |
| `data`                                         | array                      | Array of [Post objects](#post-object-tiktokpost)                |
| `pagination.cursor` / `pagination.hasNextPage` | string / boolean, nullable | Cursor for the next page and whether more results are available |
| `pricing.creditsCharged`                       | number                     | Credits charged for this request                                |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Get Post by URL

Returns a single TikTok post by its full URL.

> **Note:** this is a distinct operation from [Get Post by ID](#get-post-by-id) above. It hits the bare `/tiktok/post` path with no path parameter, taking `url` as a query parameter instead — useful when you have a TikTok link (copied from the app or scraped off the web) but haven't extracted the numeric post ID from it.

**Request**

```bash
GET /tiktok/post
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Default | Description                                                                                               |
| --------- | ------ | -------- | ------- | --------------------------------------------------------------------------------------------------------- |
| `url`     | string | Yes      | —       | Full TikTok post URL, 1–2048 characters (e.g. `https://www.tiktok.com/@tiktok/video/7636131611674692894`) |

**Example**

```bash
curl -G "https://api.pullbay.com/tiktok/post" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d url="https://www.tiktok.com/@tiktok/video/7636131611674692894"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "7636131611674692894",
      "title": "Check this out!",
      "postPage": "https://www.tiktok.com/@tiktok/video/7636131611674692894",
      "uploadedAt": 1714560000,
      "uploadedAtFormatted": "2024-05-01T00:00:00Z",
      "views": 750000,
      "likes": 12500,
      "comments": 793,
      "shares": 1250,
      "bookmarks": 320,
      "poi": null,
      "channel": {
        "id": "107955",
        "username": "tiktok",
        "name": "TikTok",
        "bio": "One TikTok can make a big impact",
        "avatar": "https://p16-common-sign.tiktokcdn.com/tos-maliva-avt-0068/avatar.jpg"
      },
      "collabInfo": {},
      "video": {},
      "song": {},
      "hashtags": [],
      "subtitleInformation": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                      |
| ------------------------ | ------ | ---------------------------------------------------------------- |
| `data`                   | array  | Array containing a single [Post object](#post-object-tiktokpost) |
| `pricing.creditsCharged` | number | Credits charged for this request                                 |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Get Hashtag by Name

Returns a TikTok hashtag profile by name (without `#`).

**Request**

```bash
GET /tiktok/hashtag/handle/{name}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required   | Default | Description                                             |
| --------- | ------ | ---------- | ------- | ------------------------------------------------------- |
| `name`    | string | Yes (path) | —       | Hashtag name without `#`, 1–100 characters (e.g. `fyp`) |

**Example**

```bash
curl -G "https://api.pullbay.com/tiktok/hashtag/handle/fyp" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "12345",
      "name": "fyp",
      "views": 1234567890
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                               |
| ------------------------ | ------ | ------------------------------------------------------------------------- |
| `data`                   | array  | Array containing a single [Hashtag object](#hashtag-object-tiktokhashtag) |
| `pricing.creditsCharged` | number | Credits charged for this request                                          |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                     |
| ------ | ------------------------------------------- |
| `400`  | Bad request — missing or invalid parameters |
| `402`  | Insufficient credits                        |
| `404`  | User, post, comment, or hashtag not found   |

## Object Schema

### User object (`TiktokUser`)

| Field             | Type    | Description                           |
| ----------------- | ------- | ------------------------------------- |
| `id`              | string  | TikTok user ID                        |
| `username`        | string  | TikTok handle (without `@`)           |
| `nickname`        | string  | Display name                          |
| `url`             | string  | Profile URL                           |
| `bio`             | string  | Bio text                              |
| `bioUrl`          | string  | Link in bio                           |
| `avatar`          | string  | Avatar image URL                      |
| `verified`        | boolean | Whether the account is verified       |
| `profileCategory` | string  | Profile category, if set              |
| `followers`       | integer | Follower count                        |
| `following`       | integer | Following count                       |
| `likes`           | integer | Total likes received across all posts |
| `videos`          | integer | Total number of posts                 |

### Post object (`TiktokPost`)

| Field                 | Type               | Description                                 |
| --------------------- | ------------------ | ------------------------------------------- |
| `id`                  | string             | TikTok post ID                              |
| `title`               | string             | Post caption/title                          |
| `postPage`            | string             | URL of the post                             |
| `uploadedAt`          | integer            | Upload time, Unix timestamp in seconds      |
| `uploadedAtFormatted` | string (date-time) | Upload time, ISO 8601                       |
| `views`               | integer            | View count                                  |
| `likes`               | integer            | Like count                                  |
| `comments`            | integer            | Comment count                               |
| `shares`              | integer            | Share count                                 |
| `bookmarks`           | integer            | Bookmark (save) count                       |
| `poi`                 | object, nullable   | Point-of-interest tag, if present           |
| `channel`             | object             | Post author — see Post Channel object below |
| `collabInfo`          | object             | Collaborator info, if the post is a collab  |
| `video`               | object             | Video asset details                         |
| `song`                | object             | Sound/music details                         |
| `hashtags`            | array              | Hashtags used in the post                   |
| `subtitleInformation` | array              | Available subtitle/caption tracks           |

### Post Channel object (`TiktokPostChannel`)

Lightweight author reference embedded in `TiktokPost.channel`.

| Field      | Type   | Description                 |
| ---------- | ------ | --------------------------- |
| `id`       | string | TikTok user ID              |
| `username` | string | TikTok handle (without `@`) |
| `name`     | string | Display name                |
| `bio`      | string | Bio text                    |
| `avatar`   | string | Avatar image URL            |

### Comment object (`TiktokComment`)

Used for both top-level comments and replies.

| Field             | Type               | Description                                                         |
| ----------------- | ------------------ | ------------------------------------------------------------------- |
| `id`              | string             | Comment ID                                                          |
| `parentId`        | string             | Parent comment ID — present only on replies                         |
| `awemeId`         | string             | ID of the post the comment was made on                              |
| `text`            | string             | Comment text                                                        |
| `createdAt`       | string (date-time) | When the comment was posted                                         |
| `likeCount`       | integer            | Like count                                                          |
| `replyCount`      | integer            | Number of replies — present only on top-level comments              |
| `commentLanguage` | string             | Detected language code                                              |
| `isAuthorLiked`   | boolean            | Whether the post author liked this comment                          |
| `user`            | object             | Commenter info (`id`, `username`, `nickname`, `avatar`, `verified`) |

### Hashtag object (`TiktokHashtag`)

| Field   | Type    | Description                                         |
| ------- | ------- | --------------------------------------------------- |
| `id`    | string  | Hashtag ID                                          |
| `name`  | string  | Hashtag name (without `#`)                          |
| `views` | integer | Total view count across all posts using the hashtag |

## Finding User and Post IDs

* **User ID**: returned as `id` from `GET /tiktok/user/handle/{username}`. You need this numeric ID to call `GET /tiktok/user/{id}/posts`.
* **Post ID**: the long numeric segment in a TikTok video URL, e.g. `https://www.tiktok.com/@tiktok/video/7636131611674692894` → `7636131611674692894`. If you only have the URL and not the extracted ID, pass the full URL to `GET /tiktok/post` instead — no need to parse it yourself.
* **Comment ID**: returned as `id` on each item in the response from `GET /tiktok/post/{id}/comments`; required as `commentId` to fetch that comment's replies.
* **Hashtag name**: the text after `#` on a TikTok hashtag page, without the `#` (e.g. `fyp`).

## Python Example

This example fetches a user's posts a page at a time using `cursor`-based pagination, stopping when `pagination.hasNextPage` is falsy or a page cap is reached:

```python
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.pullbay.com"

def get_all_user_posts(user_id, region="US", max_pages=10):
    all_posts = []
    cursor = None
    for _ in range(max_pages):
        params = {"region": region}
        if cursor:
            params["cursor"] = cursor
        resp = requests.get(
            f"{BASE_URL}/tiktok/user/{user_id}/posts",
            headers={"Authorization": f"Bearer {API_KEY}"},
            params=params,
        )
        resp.raise_for_status()
        body = resp.json()
        all_posts.extend(body["data"])
        pagination = body.get("pagination") or {}
        cursor = pagination.get("cursor")
        if not pagination.get("hasNextPage") or not cursor:
            break
    return all_posts

posts = get_all_user_posts("107955")
print(f"Fetched {len(posts)} posts")
```

Each request is billed independently, with the charged amount reported in that response's `pricing.creditsCharged` — there's no separate "fetch everything in one call" endpoint, so a multi-page pull like this makes one request per page.

## FAQ

<details>

<summary>How do I find a TikTok user's numeric ID?</summary>

Call `GET /tiktok/user/handle/{username}` with the user's handle (without `@`). The response's `id` field is the numeric user ID you need for `GET /tiktok/user/{id}/posts`.

</details>

<details>

<summary>What values does `region` accept?</summary>

The spec constrains it to a 2–5 character string but does not document a fixed list of accepted values. Pass whatever region/country code your use case requires; the API does not validate it against a published allow-list.

</details>

<details>

<summary>What values do `dateRange` and `sortType` accept on Search Posts?</summary>

The spec leaves both genuinely undocumented — there is no enum or pattern defined for either field. Don't assume specific option strings (like a fixed set of date windows or sort orders); if you need a particular behavior, test the value against the live API rather than relying on documentation that doesn't exist yet.

</details>

<details>

<summary>What's the difference between Get Post by ID and Get Post by URL?</summary>

They're two separate endpoints for the same underlying lookup. `GET /tiktok/post/{id}` takes the numeric post ID as a path parameter. `GET /tiktok/post` takes the full post URL as a query parameter (`url`) on the bare path, with no path parameter at all. Use whichever identifier you already have.

</details>

<details>

<summary>How do I paginate through comments or search results?</summary>

Use `cursor` (pass the value from the previous response's `pagination.cursor`) or `maxItems` for a single bulk pull — the two are mutually exclusive. Stop paging when `pagination.hasNextPage` is falsy.

</details>

<details>

<summary>What's the credit cost per request?</summary>

It's dynamic — every response includes `pricing.creditsCharged` with the exact amount for that call. There's no fixed published rate per endpoint or per result.

</details>

## Next Steps

* [**API Integration Guide**](broken://pages/68e75ab1a73d266350bd68301be87ff97cad563e) — integrate the Pullbay API into your backend application.
* [**n8n Integration Guide**](broken://pages/4308164455b56bbc694cf74c9768ea689c2fe719) — set up automated workflows using n8n with no coding required.
* [**Services Overview**](broken://pages/0d55376cbcca70097a683a71a593d3077825dbf1) — explore all available Pullbay services.


# Twitter / X

The Pullbay Twitter API provides programmatic access to public Twitter/X data — tweet search, individual tweets, and user profiles by handle or numeric ID. Integrate Twitter data into your applications, dashboards, and analytics pipelines without scraping or using the official Twitter/X developer platform.

**Base URL:** `https://api.pullbay.com`

## Overview

The Twitter service lets you:

* Search tweets by keyword, sorted by `Top` or `Latest`
* Look up a single tweet by its numeric ID
* Look up a user profile by handle (`@username`) or by numeric user ID
* Fetch extended "about" details for a user (account region, profile image shape, username change history)
* Pull all tweets posted by a given user

All endpoints are read-only `GET` requests against public Twitter/X data — no posting, no DMs, and no authenticated account access is included.

## Common Use Cases

**Brand & Keyword Monitoring** — Search for mentions of a brand, product, or keyword and feed matching tweets into your own alerting or sentiment pipeline.

**Influencer & Competitor Tracking** — Pull a user's profile and recent tweets to track follower growth, posting cadence, and engagement trends over time.

**Content Research** — Use Search Twitter to discover what's being said about a topic, then pull the authoring accounts' profiles to understand who's driving the conversation.

**Lead & Audience Enrichment** — Resolve a handle to its numeric user ID and profile fields (bio, follower count, verification status) to enrich a CRM record or audience segment.

**Historical Tweet Lookup** — Fetch a specific tweet by ID when you already have a link or ID from another source and need the full text, engagement counts, and author details.

## Endpoints

| Endpoint                             | Description                              | Pagination        |
| ------------------------------------ | ---------------------------------------- | ----------------- |
| `GET /twitter/search`                | Search tweets by query                   | Cursor / maxItems |
| `GET /twitter/tweet/{id}`            | Get a single tweet by ID                 | —                 |
| `GET /twitter/handle/{handle}`       | Get a user by handle                     | —                 |
| `GET /twitter/handle/{handle}/about` | Get a user's about/bio details by handle | —                 |
| `GET /twitter/user/{id}`             | Get a user by numeric ID                 | —                 |
| `GET /twitter/user/{id}/tweets`      | Get tweets posted by a user              | Cursor / maxItems |

***

### Search Twitter

Search tweets matching a query.

**Request**

```bash
GET /twitter/search
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type                    | Required | Default  | Description                                                                    |
| ---------- | ----------------------- | -------- | -------- | ------------------------------------------------------------------------------ |
| `query`    | string                  | Yes      | —        | Search query, 1–100 characters                                                 |
| `sort`     | string                  | No       | `Latest` | `Top` or `Latest`                                                              |
| `useFirst` | string (`true`/`false`) | No       | `false`  | `true` or `false`                                                              |
| `maxItems` | integer                 | No       | —        | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`            |
| `cursor`   | string                  | No       | —        | Pagination cursor from a previous response. Mutually exclusive with `maxItems` |

**Example**

```bash
curl -G "https://api.pullbay.com/twitter/search" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d query="openai" \
  -d sort=Latest \
  -d maxItems=20
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "tweet",
      "id": "20",
      "url": "https://x.com/jack/status/20",
      "twitterUrl": "https://twitter.com/jack/status/20",
      "text": "just setting up my twttr",
      "fullText": "just setting up my twttr",
      "source": "Twitter Web Client",
      "lang": "en",
      "createdAt": "Tue Mar 21 20:50:14 +0000 2006",
      "conversationId": "20",
      "retweetCount": 126541,
      "replyCount": 17858,
      "likeCount": 309719,
      "quoteCount": 6384,
      "bookmarkCount": 21386,
      "viewCount": null,
      "isReply": false,
      "isPinned": false,
      "isRetweet": false,
      "isQuote": false,
      "author": {
        "type": "user",
        "id": "44196397",
        "userName": "elonmusk",
        "name": "Elon Musk",
        "url": "https://x.com/elonmusk",
        "twitterUrl": "https://twitter.com/elonmusk",
        "description": "https://t.co/dDtDyVssfm",
        "location": "",
        "profilePicture": "https://pbs.twimg.com/profile_images/2035314704307081216/71U1ftM3_normal.jpg",
        "coverPicture": "https://pbs.twimg.com/profile_banners/44196397/1774145451",
        "isVerified": true,
        "isBlueVerified": true,
        "protected": false,
        "canDm": false,
        "canMediaTag": false,
        "followers": 239698341,
        "following": 1319,
        "fastFollowersCount": 0,
        "favouritesCount": 225074,
        "listedCount": 168631,
        "mediaCount": 4467,
        "statusesCount": 101980,
        "createdAt": "Tue Jun 02 20:12:29 +0000 2009",
        "status": ""
      },
      "media": []
    }
  ],
  "pagination": { "page": null, "hasNextPage": true, "cursor": "DAACCgACGY...", "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type              | Description                                    |
| ------------------------ | ----------------- | ---------------------------------------------- |
| `data`                   | array             | Array of [Tweet objects](#tweet-object)        |
| `pagination.page`        | null              | Unused by this endpoint — always `null` here   |
| `pagination.hasNextPage` | boolean, nullable | Whether another page is available              |
| `pagination.cursor`      | string, nullable  | Cursor to pass as `cursor` on the next request |
| `pagination.offset`      | null              | Unused by this endpoint — always `null` here   |
| `pricing.creditsCharged` | number            | Credits charged for this request               |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate — read the figure from each response.

***

### Get Tweet

Returns a single tweet by its numeric ID.

**Request**

```bash
GET /twitter/tweet/{id}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Default | Description                                    |
| --------- | ------ | -------- | ------- | ---------------------------------------------- |
| `id`      | string | Yes      | —       | Numeric tweet ID, 1–100 characters (e.g. `20`) |

**Example**

```bash
curl -G "https://api.pullbay.com/twitter/tweet/20" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "tweet",
      "id": "20",
      "url": "https://x.com/jack/status/20",
      "twitterUrl": "https://twitter.com/jack/status/20",
      "text": "just setting up my twttr",
      "fullText": "just setting up my twttr",
      "source": "Twitter Web Client",
      "lang": "en",
      "createdAt": "Tue Mar 21 20:50:14 +0000 2006",
      "conversationId": "20",
      "retweetCount": 126541,
      "replyCount": 17858,
      "likeCount": 309719,
      "quoteCount": 6384,
      "bookmarkCount": 21386,
      "viewCount": null,
      "isReply": false,
      "isPinned": false,
      "isRetweet": false,
      "isQuote": false,
      "author": {
        "type": "user",
        "id": "12",
        "userName": "jack",
        "name": "jack",
        "url": "https://x.com/jack",
        "twitterUrl": "https://twitter.com/jack",
        "description": "",
        "location": "",
        "profilePicture": "https://pbs.twimg.com/profile_images/1115644092329758721/AFjOzXz3_normal.jpg",
        "coverPicture": "",
        "isVerified": false,
        "isBlueVerified": false,
        "protected": false,
        "canDm": false,
        "canMediaTag": false,
        "followers": 6800000,
        "following": 4500,
        "fastFollowersCount": 0,
        "favouritesCount": 25000,
        "listedCount": 22000,
        "mediaCount": 350,
        "statusesCount": 30000,
        "createdAt": "Tue Mar 21 20:50:14 +0000 2006",
        "status": ""
      },
      "media": []
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                             |
| ------------------------ | ------ | ------------------------------------------------------- |
| `data`                   | array  | Array containing a single [Tweet object](#tweet-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                        |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No published flat rate.

***

### Get User By Handle

Returns a Twitter user by handle (without `@`).

**Request**

```bash
GET /twitter/handle/{handle}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Default | Description                                                    |
| --------- | ------ | -------- | ------- | -------------------------------------------------------------- |
| `handle`  | string | Yes      | —       | Twitter handle without `@`, 1–100 characters (e.g. `elonmusk`) |

**Example**

```bash
curl -G "https://api.pullbay.com/twitter/handle/elonmusk" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "user",
      "id": "44196397",
      "userName": "elonmusk",
      "name": "Elon Musk",
      "url": "https://x.com/elonmusk",
      "twitterUrl": "https://twitter.com/elonmusk",
      "description": "https://t.co/dDtDyVssfm",
      "location": "",
      "profilePicture": "https://pbs.twimg.com/profile_images/2035314704307081216/71U1ftM3_normal.jpg",
      "coverPicture": "https://pbs.twimg.com/profile_banners/44196397/1774145451",
      "isVerified": true,
      "isBlueVerified": true,
      "protected": false,
      "canDm": false,
      "canMediaTag": false,
      "followers": 239698341,
      "following": 1319,
      "fastFollowersCount": 0,
      "favouritesCount": 225074,
      "listedCount": 168631,
      "mediaCount": 4467,
      "statusesCount": 101980,
      "createdAt": "Tue Jun 02 20:12:29 +0000 2009",
      "status": ""
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                           |
| ------------------------ | ------ | ----------------------------------------------------- |
| `data`                   | array  | Array containing a single [User object](#user-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                      |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get User About By Handle

Returns extended profile / about details for a Twitter user, by handle.

**Request**

```bash
GET /twitter/handle/{handle}/about
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Default | Description                                                    |
| --------- | ------ | -------- | ------- | -------------------------------------------------------------- |
| `handle`  | string | Yes      | —       | Twitter handle without `@`, 1–100 characters (e.g. `elonmusk`) |

**Example**

```bash
curl -G "https://api.pullbay.com/twitter/handle/elonmusk/about" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "accountBasedIn": "United States",
      "locationAccurate": true,
      "profileImageShape": "Circle",
      "avatarUrl": "https://pbs.twimg.com/profile_images/2035314704307081216/71U1ftM3_normal.jpg",
      "usernameChangeCount": "0",
      "accountCreatedAt": "Tue Jun 02 20:12:29 +0000 2009",
      "accountSource": "United States App Store",
      "verifiedSince": "-156836000000000"
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                                       |
| ------------------------ | ------ | ----------------------------------------------------------------- |
| `data`                   | array  | Array containing a single [User About object](#user-about-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                                  |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get User By ID

Returns a Twitter user by their numeric user ID.

**Request**

```bash
GET /twitter/user/{id}
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter | Type   | Required | Default | Description                                                 |
| --------- | ------ | -------- | ------- | ----------------------------------------------------------- |
| `id`      | string | Yes      | —       | Numeric Twitter user ID, 1–100 characters (e.g. `44196397`) |

**Example**

```bash
curl -G "https://api.pullbay.com/twitter/user/44196397" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "user",
      "id": "44196397",
      "userName": "elonmusk",
      "name": "Elon Musk",
      "url": "https://x.com/elonmusk",
      "twitterUrl": "https://twitter.com/elonmusk",
      "description": "https://t.co/dDtDyVssfm",
      "location": "",
      "profilePicture": "https://pbs.twimg.com/profile_images/2035314704307081216/71U1ftM3_normal.jpg",
      "coverPicture": "https://pbs.twimg.com/profile_banners/44196397/1774145451",
      "isVerified": true,
      "isBlueVerified": true,
      "protected": false,
      "canDm": false,
      "canMediaTag": false,
      "followers": 239698341,
      "following": 1319,
      "fastFollowersCount": 0,
      "favouritesCount": 225074,
      "listedCount": 168631,
      "mediaCount": 4467,
      "statusesCount": 101980,
      "createdAt": "Tue Jun 02 20:12:29 +0000 2009",
      "status": ""
    }
  ],
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type   | Description                                           |
| ------------------------ | ------ | ----------------------------------------------------- |
| `data`                   | array  | Array containing a single [User object](#user-object) |
| `pricing.creditsCharged` | number | Credits charged for this request                      |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`.

***

### Get User's Tweets

Returns tweets posted by a user, by user ID.

**Request**

```bash
GET /twitter/user/{id}/tweets
Authorization: Bearer YOUR_API_KEY
```

**Parameters**

| Parameter  | Type    | Required | Default | Description                                                                    |
| ---------- | ------- | -------- | ------- | ------------------------------------------------------------------------------ |
| `id`       | string  | Yes      | —       | Numeric Twitter user ID, 1–100 characters (e.g. `44196397`)                    |
| `maxItems` | integer | No       | —       | One-shot bulk pull, 1–200 results. Mutually exclusive with `cursor`            |
| `cursor`   | string  | No       | —       | Pagination cursor from a previous response. Mutually exclusive with `maxItems` |

**Example**

```bash
curl -G "https://api.pullbay.com/twitter/user/44196397/tweets" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d maxItems=20
```

**Response**

```json
{
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "type": "tweet",
      "id": "20",
      "url": "https://x.com/jack/status/20",
      "twitterUrl": "https://twitter.com/jack/status/20",
      "text": "just setting up my twttr",
      "fullText": "just setting up my twttr",
      "source": "Twitter Web Client",
      "lang": "en",
      "createdAt": "Tue Mar 21 20:50:14 +0000 2006",
      "conversationId": "20",
      "retweetCount": 126541,
      "replyCount": 17858,
      "likeCount": 309719,
      "quoteCount": 6384,
      "bookmarkCount": 21386,
      "viewCount": null,
      "isReply": false,
      "isPinned": false,
      "isRetweet": false,
      "isQuote": false,
      "author": {
        "type": "user",
        "id": "44196397",
        "userName": "elonmusk",
        "name": "Elon Musk",
        "url": "https://x.com/elonmusk",
        "twitterUrl": "https://twitter.com/elonmusk",
        "description": "https://t.co/dDtDyVssfm",
        "location": "",
        "profilePicture": "https://pbs.twimg.com/profile_images/2035314704307081216/71U1ftM3_normal.jpg",
        "coverPicture": "https://pbs.twimg.com/profile_banners/44196397/1774145451",
        "isVerified": true,
        "isBlueVerified": true,
        "protected": false,
        "canDm": false,
        "canMediaTag": false,
        "followers": 239698341,
        "following": 1319,
        "fastFollowersCount": 0,
        "favouritesCount": 225074,
        "listedCount": 168631,
        "mediaCount": 4467,
        "statusesCount": 101980,
        "createdAt": "Tue Jun 02 20:12:29 +0000 2009",
        "status": ""
      },
      "media": []
    }
  ],
  "pagination": { "page": null, "hasNextPage": true, "cursor": "DAACCgACGY...", "offset": null },
  "pricing": { "creditsCharged": 1 }
}
```

**Response Schema**

| Field                    | Type              | Description                                    |
| ------------------------ | ----------------- | ---------------------------------------------- |
| `data`                   | array             | Array of [Tweet objects](#tweet-object)        |
| `pagination.page`        | null              | Unused by this endpoint — always `null` here   |
| `pagination.hasNextPage` | boolean, nullable | Whether another page is available              |
| `pagination.cursor`      | string, nullable  | Cursor to pass as `cursor` on the next request |
| `pagination.offset`      | null              | Unused by this endpoint — always `null` here   |
| `pricing.creditsCharged` | number            | Credits charged for this request               |

**Credit cost:** dynamic, returned in `pricing.creditsCharged`. No flat per-request rate is published — read the figure from each response.

***

## Errors

All non-2xx responses share the same envelope:

```json
{
  "status": 400,
  "message": "Bad request",
  "success": false,
  "error": { "code": "BAD_REQUEST" }
}
```

| Status | Meaning                                  |
| ------ | ---------------------------------------- |
| `400`  | Bad request — missing/invalid parameters |
| `402`  | Insufficient credits                     |
| `404`  | Tweet or user not found                  |

## Object Schema

### Tweet object

| Field                                                                        | Type                        | Description                                  |
| ---------------------------------------------------------------------------- | --------------------------- | -------------------------------------------- |
| `type`                                                                       | string                      | Always `tweet`                               |
| `id`                                                                         | string                      | Numeric tweet ID                             |
| `url` / `twitterUrl`                                                         | string                      | Tweet permalink on `x.com` and `twitter.com` |
| `text` / `fullText`                                                          | string                      | Tweet body (truncated and full)              |
| `source`                                                                     | string                      | Client used to post the tweet                |
| `lang`                                                                       | string                      | Tweet language code                          |
| `createdAt`                                                                  | string                      | Posted timestamp (Twitter date format)       |
| `conversationId`                                                             | string                      | ID of the root tweet in the thread           |
| `retweetCount` / `replyCount` / `likeCount` / `quoteCount` / `bookmarkCount` | integer                     | Engagement counts                            |
| `viewCount`                                                                  | integer, nullable           | View count, when available                   |
| `isReply` / `isPinned` / `isRetweet` / `isQuote`                             | boolean                     | Tweet type flags                             |
| `author`                                                                     | [User object](#user-object) | The tweet's author                           |
| `media`                                                                      | array                       | Attached media objects                       |

### User object

| Field                             | Type    | Description                                           |
| --------------------------------- | ------- | ----------------------------------------------------- |
| `type`                            | string  | Always `user`                                         |
| `id`                              | string  | Numeric Twitter user ID                               |
| `userName`                        | string  | Handle, without `@`                                   |
| `name`                            | string  | Display name                                          |
| `url` / `twitterUrl`              | string  | Profile URL on `x.com` and `twitter.com`              |
| `description`                     | string  | Bio text                                              |
| `location`                        | string  | Free-text profile location                            |
| `profilePicture` / `coverPicture` | string  | Avatar and banner image URLs                          |
| `isVerified` / `isBlueVerified`   | boolean | Legacy verification and Blue subscriber verification  |
| `protected`                       | boolean | Whether the account's tweets are protected            |
| `canDm` / `canMediaTag`           | boolean | Whether the user accepts DMs / can be tagged in media |
| `followers` / `following`         | integer | Follower and following counts                         |
| `fastFollowersCount`              | integer | Recently-gained follower count                        |
| `favouritesCount`                 | integer | Total likes given by the user                         |
| `listedCount`                     | integer | Number of lists the user appears on                   |
| `mediaCount`                      | integer | Number of tweets containing media                     |
| `statusesCount`                   | integer | Total tweet count                                     |
| `createdAt`                       | string  | Account creation timestamp (Twitter date format)      |
| `status`                          | string  | Reserved/internal status field                        |

Note: `User object` is the schema returned by both Get User By Handle and Get User By ID — it is a distinct object from `User About object` below, which only the about/bio endpoint returns.

### User about object

| Field                 | Type    | Description                                                        |
| --------------------- | ------- | ------------------------------------------------------------------ |
| `accountBasedIn`      | string  | Inferred account region                                            |
| `locationAccurate`    | boolean | Whether `accountBasedIn` is considered accurate                    |
| `profileImageShape`   | string  | Profile picture mask shape (e.g. `Circle`)                         |
| `avatarUrl`           | string  | Avatar image URL                                                   |
| `usernameChangeCount` | string  | Number of times the handle has been changed (returned as a string) |
| `accountCreatedAt`    | string  | Account creation timestamp (Twitter date format)                   |
| `accountSource`       | string  | Platform/channel the account was created from                      |
| `verifiedSince`       | string  | Verification timestamp, when applicable (returned as a string)     |

Note: the User About object does not include `id`, `userName`, or any other linking field back to the User object — correlate the two by the `handle` you requested, not by a shared key in the response body.

## Finding User and Tweet IDs

* **Handle**: the `@username` shown on a profile or in a tweet URL, without the `@` (e.g. `https://x.com/elonmusk` → `elonmusk`). Pass this to the `/twitter/handle/{handle}` and `/twitter/handle/{handle}/about` endpoints.
* **Numeric user ID**: a stable internal ID, distinct from the handle. You can get it from a prior handle lookup's `id` field, or from a tweet's `author.id`. Pass this to `/twitter/user/{id}` and `/twitter/user/{id}/tweets`. Numeric IDs don't change even if a user changes their handle, so prefer caching the ID for repeated lookups.
* **Tweet ID**: the numeric ID in a tweet's URL, e.g. `https://x.com/jack/status/20` → `20`. Pass this to `/twitter/tweet/{id}`.
* Handle and numeric ID resolve to the same user but are not interchangeable inputs — each endpoint accepts only one or the other, not both.

## Python Example

This example fetches all tweets for a user a page at a time, following the `cursor` returned in `pagination.cursor` until `pagination.hasNextPage` is `false` or the page cap is reached:

```python
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.pullbay.com"

def get_all_user_tweets(user_id, max_pages=10):
    all_tweets = []
    cursor = None
    pages_fetched = 0
    while pages_fetched < max_pages:
        params = {}
        if cursor:
            params["cursor"] = cursor
        resp = requests.get(
            f"{BASE_URL}/twitter/user/{user_id}/tweets",
            headers={"Authorization": f"Bearer {API_KEY}"},
            params=params,
        )
        resp.raise_for_status()
        body = resp.json()
        all_tweets.extend(body["data"])
        pagination = body.get("pagination") or {}
        if not pagination.get("hasNextPage"):
            break
        cursor = pagination.get("cursor")
        pages_fetched += 1
    return all_tweets

tweets = get_all_user_tweets("44196397")
print(f"Fetched {len(tweets)} tweets")
```

Each request is billed independently, with the charged amount reported in that response's `pricing.creditsCharged` — there's no separate "fetch everything in one call" endpoint, so a multi-page pull like this makes one request per page.

## FAQ

<details>

<summary>How do I find a user's numeric ID if I only have their handle?</summary>

Call `/twitter/handle/{handle}` first — the response's `id` field is the numeric user ID. You can also get a user's numeric ID from `author.id` on any of their tweets.

</details>

<details>

<summary>What's the difference between the basic user lookup and the "about" endpoint?</summary>

The basic lookup (`/twitter/handle/{handle}` or `/twitter/user/{id}`) returns the User object: bio, follower counts, verification status, and engagement counters. The about endpoint (`/twitter/handle/{handle}/about`) returns a separate User About object with different fields — inferred account region, profile image shape, username change count, and verification timestamp. The about endpoint is only available by handle, not by numeric ID.

</details>

<details>

<summary>Is <code>useFirst</code> a boolean?</summary>

No. It's documented and validated as a string with the literal enum values `true` and `false`, not a JSON boolean. Send it as the string `"true"` or `"false"` (e.g. `useFirst=true` in a query string), not as a boolean literal.

</details>

<details>

<summary>How many tweets can I fetch in one call?</summary>

Up to 200 with `maxItems` on Search Twitter or Get User's Tweets. `cursor` and `maxItems` are mutually exclusive — use one or the other per request.

</details>

<details>

<summary>Can I sort search results?</summary>

Yes — `sort` accepts `Top` or `Latest` (default `Latest`). There's no other sort order published for search.

</details>

<details>

<summary>What's the credit cost per request?</summary>

It's dynamic — every response includes `pricing.creditsCharged` with the exact amount for that call. There's no fixed published rate per endpoint or per result.

</details>


# Errors and Retries

Understand Pullbay API error codes, HTTP status codes, and how to implement robust retry logic.

### Error Response Format

All Pullbay API errors follow a consistent JSON envelope:

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 400,
  "message": "Validation failed",
  "success": false,
  "error": { "code": "VALIDATION_ERROR" }
}
```

| Field        | Type    | Description                                                              |
| ------------ | ------- | ------------------------------------------------------------------------ |
| `requestId`  | string  | Unique identifier for this request. Save this for debugging and support. |
| `status`     | integer | HTTP status code mirrored in the body                                    |
| `message`    | string  | Human-readable description of what went wrong                            |
| `success`    | boolean | Always `false` for errors                                                |
| `error.code` | string  | Machine-readable error code for programmatic handling                    |

The `requestId` is also returned as the `X-Request-Id` response header.

**Always include `requestId` when contacting Pullbay support** — it allows the support team to instantly locate your request in the logs.

***

### HTTP Status Codes Reference

| Status  | Name                  | Retryable? | Meaning                        | What to Do                           |
| ------- | --------------------- | ---------- | ------------------------------ | ------------------------------------ |
| **200** | OK                    | N/A        | Request successful             | Process the response data            |
| **400** | Bad Request           | No         | Invalid parameters or format   | Fix your request and resubmit        |
| **401** | Unauthorized          | No         | Invalid or missing API key     | Verify your API key                  |
| **402** | Payment Required      | No         | Insufficient credits           | Add credits in your dashboard        |
| **403** | Forbidden             | No         | API key lacks permission       | Use a key with the right permissions |
| **404** | Not Found             | No         | Endpoint or resource not found | Check the endpoint URL               |
| **429** | Too Many Requests     | Yes        | Rate limit exceeded            | Back off and wait for reset          |
| **500** | Internal Server Error | Yes        | Temporary server error         | Wait and retry with backoff          |
| **502** | Bad Gateway           | Yes        | Upstream service unavailable   | Wait and retry                       |
| **503** | Service Unavailable   | Yes        | Maintenance or overload        | Wait longer before retrying          |

***

### Non-Retryable Errors (4xx except 429)

Do **not** retry requests that return `400`, `401`, `402`, `403`, or `404`. These indicate problems with your request that won't resolve by waiting:

```python
NON_RETRYABLE_CODES = {400, 401, 402, 403, 404}

if response.status_code in NON_RETRYABLE_CODES:
    body = response.json()
    print(f"Non-retryable error: {body['error']['code']}")
    print(f"Message: {body['message']}")
    print(f"Request ID: {body['requestId']}")
    # Fix the underlying issue; don't retry
```

### Retryable Errors (429 and 5xx)

These errors are usually temporary and will likely succeed if retried with backoff:

```python
RETRYABLE_CODES = {429, 500, 502, 503}

if response.status_code in RETRYABLE_CODES:
    print(f"Transient error {response.status_code} — implement backoff and retry")
```

***

### Common Error Codes

#### Authentication Errors

| Code              | HTTP Status | Description                           | Solution                              |
| ----------------- | ----------- | ------------------------------------- | ------------------------------------- |
| `INVALID_API_KEY` | 401         | API key is malformed or doesn't exist | Check your API key format and value   |
| `MISSING_API_KEY` | 401         | Authorization header is missing       | Include `Authorization: Bearer <key>` |

**Correct format:**

```python
headers = {
    "Authorization": "Bearer your_api_key_here"
}
# NOT: "Authorization": your_api_key_here"   (missing "Bearer ")
# NOT: "x-api-key: your_api_key_here"       (also accepted, but Bearer preferred)
```

#### Request Parameter Errors

| Code               | HTTP Status | Description                               | Solution                              |
| ------------------ | ----------- | ----------------------------------------- | ------------------------------------- |
| `VALIDATION_ERROR` | 400         | Parameter has invalid value or wrong type | Check parameter types and constraints |

**Example:**

```json
{
  "requestId": "3b8dcb68-...",
  "status": 400,
  "message": "Validation failed",
  "success": false,
  "error": { "code": "VALIDATION_ERROR" }
}
```

**Fix — include all required parameters with correct types:**

```python
# Missing required parameter
params = {"maxItems": 50}  # Missing 'location'!

# Correct
params = {
    "location": "Seattle, WA",  # Required
    "maxItems": 50              # Optional
}
```

#### Credit Errors

| Code                   | HTTP Status | Description                        | Solution                                  |
| ---------------------- | ----------- | ---------------------------------- | ----------------------------------------- |
| `INSUFFICIENT_CREDITS` | 402         | Account has run out of API credits | Add credits via dashboard or upgrade plan |

**Example:**

```json
{
  "requestId": "3b8dcb68-...",
  "status": 402,
  "message": "Insufficient credits",
  "success": false,
  "error": { "code": "INSUFFICIENT_CREDITS" }
}
```

**Do not retry a 402** — user action is required (top up credits):

```python
if response.status_code == 402:
    body = response.json()
    print(f"Out of credits. Request ID: {body['requestId']}")
    print("Visit your Pullbay dashboard to add credits.")
    # Do not retry
```

#### Rate Limit Errors

| Code                  | HTTP Status | Description                             | Solution                                  |
| --------------------- | ----------- | --------------------------------------- | ----------------------------------------- |
| `RATE_LIMIT_EXCEEDED` | 429         | Per-second or per-minute rate limit hit | Back off and retry after the reset window |

Rate limits are enforced per API key on both a per-second and a per-minute basis based on your plan. The response headers indicate the current limit state:

```
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1712973600
```

**Handle a 429:**

```python
if response.status_code == 429:
    reset_timestamp = int(response.headers.get("X-RateLimit-Reset", 0))
    wait_seconds = max(reset_timestamp - time.time(), 1)
    print(f"Rate limited. Waiting {wait_seconds:.1f}s...")
    time.sleep(wait_seconds + 1)
    # Then retry
```

#### Server Errors

| Code | HTTP Status | Description                     | Solution                       |
| ---- | ----------- | ------------------------------- | ------------------------------ |
| —    | 500         | Unexpected server error         | Retry with exponential backoff |
| —    | 502         | Upstream service unavailable    | Retry with backoff             |
| —    | 503         | Service maintenance or overload | Wait longer before retrying    |

***

### Retryable vs. Non-Retryable Summary

| Error Type           | Status Codes  | Retry?                    |
| -------------------- | ------------- | ------------------------- |
| Authentication       | 401           | No — fix your key         |
| Bad request          | 400           | No — fix your parameters  |
| Insufficient credits | 402           | No — add credits          |
| Forbidden            | 403           | No — check permissions    |
| Not found            | 404           | No — check endpoint URL   |
| Rate limited         | 429           | Yes — wait for reset      |
| Server errors        | 500, 502, 503 | Yes — exponential backoff |
| Network timeout      | —             | Yes — retry with backoff  |

***

### Production-Ready Python Retry Implementation

```python
import time
import random
import requests

API_KEY = "your_api_key_here"
BASE_URL = "https://api.pullbay.com"

RETRYABLE_CODES = {429, 500, 502, 503}
NON_RETRYABLE_CODES = {400, 401, 402, 403, 404}


class PullbayClient:
    def __init__(self, api_key, max_retries=5, base_delay=1):
        self.api_key = api_key
        self.max_retries = max_retries
        self.base_delay = base_delay

    def get(self, endpoint, params=None):
        headers = {
            "Authorization": f"Bearer {self.api_key}",
        }

        for attempt in range(self.max_retries + 1):
            try:
                response = requests.get(
                    f"{BASE_URL}{endpoint}",
                    params=params,
                    headers=headers,
                    timeout=30,
                )

                if response.status_code == 200:
                    return response.json()

                elif response.status_code in NON_RETRYABLE_CODES:
                    body = response.json()
                    raise Exception(
                        f"Non-retryable error {response.status_code}: "
                        f"{body['error']['code']} — {body['message']} "
                        f"(requestId: {body['requestId']})"
                    )

                elif response.status_code in RETRYABLE_CODES:
                    if attempt < self.max_retries:
                        wait = self._backoff(attempt, response)
                        print(
                            f"Retryable error {response.status_code} "
                            f"on attempt {attempt + 1}. "
                            f"Waiting {wait:.1f}s..."
                        )
                        time.sleep(wait)
                    else:
                        body = response.json()
                        raise Exception(
                            f"Max retries ({self.max_retries}) exceeded. "
                            f"Last error: {response.status_code} — {body['message']}"
                        )

            except requests.Timeout:
                if attempt < self.max_retries:
                    wait = self._backoff(attempt, None)
                    print(f"Timeout on attempt {attempt + 1}. Waiting {wait:.1f}s...")
                    time.sleep(wait)
                else:
                    raise Exception("Max retries exceeded: Timeout")

            except requests.ConnectionError as e:
                if attempt < self.max_retries:
                    wait = self._backoff(attempt, None)
                    print(f"Connection error on attempt {attempt + 1}: {e}. Waiting {wait:.1f}s...")
                    time.sleep(wait)
                else:
                    raise

        raise Exception("All retry attempts exhausted")

    def _backoff(self, attempt, response):
        if response and response.status_code == 429:
            reset = int(response.headers.get("X-RateLimit-Reset", 0))
            if reset > 0:
                return max(reset - time.time(), 1) + 1  # +1s buffer

        # Exponential backoff with 10% jitter
        delay = (2 ** attempt) * self.base_delay
        jitter = random.uniform(0, delay * 0.1)
        return delay + jitter


# Usage

client = PullbayClient(api_key=API_KEY, max_retries=5)

try:
    result = client.get(
        "/apartments/properties",
        params={"location": "Seattle, WA", "maxItems": 50}
    )
    data = result["data"]
    credits_charged = result["pricing"]["creditsCharged"]
    request_id = result["requestId"]
    print(f"Got {len(data)} results. Credits charged: {credits_charged}. Request ID: {request_id}")
except Exception as e:
    print(f"Failed: {e}")
```

***

### Debugging Tips

{% stepper %}
{% step %}

## Always log the `requestId`

Every response includes a `requestId`. Log it alongside every API call so you can reference it when contacting support.

```python
result = client.get("/apartments/properties", params={"location": "Seattle, WA"})
print(f"Request ID: {result['requestId']}")  # Save this in your logs
```

{% endstep %}

{% step %}

## Verify your parameters

Common mistakes:

* Missing required parameters
* Wrong types (e.g. string instead of integer)
* Values outside valid ranges (e.g. `maxItems` exceeding the endpoint limit)
* Mutually exclusive parameters used together (e.g. `page` and `maxItems`)
  {% endstep %}

{% step %}

## Check your API key

* Copy-paste directly from your Pullbay dashboard
* Include the `Bearer` prefix in the header
* Check that the key has not been revoked

```python
# Correct
headers = {"Authorization": "Bearer your_api_key_here"}

# Wrong — missing "Bearer "
headers = {"Authorization": "your_api_key_here"}
```

{% endstep %}

{% step %}

## Monitor rate limits

Check rate-limit headers on every response to proactively throttle:

```python
response = requests.get(f"{BASE_URL}/apartments/properties", headers=headers)
remaining = response.headers.get("X-RateLimit-Remaining")
limit = response.headers.get("X-RateLimit-Limit")
reset = response.headers.get("X-RateLimit-Reset")
print(f"Rate limit: {remaining}/{limit}, resets at {reset}")
```

{% endstep %}

{% step %}

## Check account credit balance

A `402` means your balance is zero. Add credits in the Pullbay dashboard and do not retry the same request programmatically.
{% endstep %}

{% step %}

## For 5xx errors, retry with backoff

`500`, `502`, and `503` are temporary. The recommended strategy is exponential backoff with jitter (see the implementation above).
{% endstep %}
{% endstepper %}

### Frequently Asked Questions

<details>

<summary>Are credits charged for failed requests?</summary>

* `429` (rate limit) — credits are **not** charged.
* `4xx` parameter errors — credits are **not** charged.
* `5xx` transient errors — credits are generally **not** charged, but check `pricing.creditsCharged` in the response to confirm.
* Successful requests (`200`) — credits **are** charged as shown in `pricing.creditsCharged`.

</details>

<details>

<summary>What does <code>VALIDATION_ERROR</code> mean?</summary>

It means one or more query parameters failed validation — missing a required field, a value outside the allowed range, or mutually exclusive parameters used together. Check the `message` field and compare your request against the endpoint's parameter table.

</details>

<details>

<summary>How do I handle rate limits gracefully?</summary>

Read the `X-RateLimit-Reset` header and wait until that Unix timestamp before retrying. The Python example above handles this automatically.

</details>

<details>

<summary>Can I increase retry attempts?</summary>

Yes — pass a higher `max_retries` to the client constructor. However, be aware that aggressive retrying during a sustained outage can look like a DDoS. For sustained failures, implement a circuit breaker that stops retrying after a threshold and alerts your on-call team.

</details>

<details>

<summary>What is <code>X-Request-Id</code>?</summary>

It is the same value as `requestId` in the response body, returned as a response header. Use whichever is more convenient for your logging layer.

</details>


# Pagination

## Overview

Pagination is essential for efficiently retrieving large datasets from APIs. Pullbay offers two pagination models to give you complete flexibility: **Standard pagination** for granular control and **Managed pagination** for simplicity. This guide explains both approaches, when to use each, and best practices for production workloads.

## Why Pagination Matters

Large datasets present three critical challenges:

1. **Performance**: Retrieving millions of records in a single request strains both client and server resources, causing timeouts and memory exhaustion.
2. **Network Efficiency**: Breaking data into smaller pages reduces bandwidth usage and allows incremental processing.
3. **Credit Control**: Pullbay charges per request. Pagination lets you fetch only what you need, controlling costs and avoiding wasted credits on unused data.

Without pagination, a single request for 100,000 items would consume massive credits and likely fail. Pagination lets you process data incrementally, caching and stopping when you have enough results.

## Standard Pagination (You Control the Flow)

Standard pagination gives you complete control: you request pages one at a time using the `cursor` parameter, process each, and decide whether to continue. This model is ideal when you need only a subset of data or want to implement custom logic.

### How It Works

1. Make an initial request to the endpoint without a cursor
2. Receive the first page of results plus a `cursor` token in the `pagination` block
3. Request the next page by passing `cursor=<token>`
4. Repeat until `hasNextPage` is `false`

### Standard Pagination Response Format

Every paginated response includes a `pagination` block. Fields that are not relevant to the current endpoint are returned as `null`.

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    { "id": "rev_001", "text": "Great service!", "rating": 5 },
    { "id": "rev_002", "text": "Good experience", "rating": 4 }
  ],
  "pagination": {
    "page": null,
    "hasNextPage": true,
    "cursor": "eyJpZCI6InJldl8wMDIifQ==",
    "offset": null
  },
  "pricing": { "creditsCharged": 52 }
}
```

When `hasNextPage` is `false`, no more pages exist — stop paginating.

### Example: First Request (No Cursor)

```bash
curl -X GET "https://api.pullbay.com/appstore/reviews" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -G \
  --data-urlencode "appId=389801252" \
  --data-urlencode "country=us"
```

### Example: Second Request (With Cursor)

```bash
curl -X GET "https://api.pullbay.com/appstore/reviews" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -G \
  --data-urlencode "appId=389801252" \
  --data-urlencode "country=us" \
  --data-urlencode "cursor=eyJpZCI6InJldl8wMDIifQ=="
```

When the final page is returned, `hasNextPage` is `false` and `cursor` is `null`:

```json
{
  "requestId": "...",
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [{ "id": "rev_951", "text": "Excellent work", "rating": 5 }],
  "pagination": {
    "page": null,
    "hasNextPage": false,
    "cursor": null,
    "offset": null
  },
  "pricing": { "creditsCharged": 2 }
}
```

### Page-Based Pagination

Some endpoints support `page` instead of `cursor`. Pass `page=<number>` (1-indexed) and the response will mirror it in `pagination.page`:

```bash
curl -X GET "https://api.pullbay.com/google-search/search" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -G \
  --data-urlencode "query=pullbay api" \
  --data-urlencode "page=2"
```

Response:

```json
{
  "requestId": "...",
  "status": 200,
  "success": true,
  "data": [...],
  "pagination": {
    "page": 2,
    "hasNextPage": true,
    "cursor": null,
    "offset": null
  },
  "pricing": { "creditsCharged": 25 }
}
```

### Complete Python Implementation

```python
import requests
import time

class PullbayPaginator:
    def __init__(self, api_key, base_url="https://api.pullbay.com"):
        self.api_key = api_key
        self.base_url = base_url
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {api_key}",
        })

    def paginate(self, endpoint, params=None, max_results=None):
        """
        Generator that yields individual items across all cursor-paginated pages.

        Args:
            endpoint: API endpoint e.g. 'appstore/reviews'
            params: Query parameters dict
            max_results: Stop after this many results (None = all)

        Yields:
            Individual items from all pages
        """
        if params is None:
            params = {}

        cursor = None
        items_fetched = 0

        while True:
            req_params = dict(params)
            if cursor:
                req_params["cursor"] = cursor

            try:
                response = self.session.get(
                    f"{self.base_url}/{endpoint}",
                    params=req_params,
                    timeout=30,
                )
                response.raise_for_status()
            except requests.exceptions.RequestException as e:
                print(f"Request failed: {e}")
                raise

            body = response.json()
            items = body.get("data", [])
            pagination = body.get("pagination", {})
            request_id = body.get("requestId")
            credits = body.get("pricing", {}).get("creditsCharged", 0)
            print(f"Page fetched — requestId: {request_id}, creditsCharged: {credits}")

            for item in items:
                yield item
                items_fetched += 1
                if max_results and items_fetched >= max_results:
                    return

            if not pagination.get("hasNextPage"):
                break

            cursor = pagination.get("cursor")
            if not cursor:
                break

            time.sleep(0.1)  # Respect rate limits

# Usage
paginator = PullbayPaginator(api_key="your_api_key")

all_reviews = []
for review in paginator.paginate(
    endpoint="appstore/reviews",
    params={"appId": "389801252", "country": "us"},
    max_results=100,
):
    all_reviews.append(review)

print(f"Total reviews fetched: {len(all_reviews)}")
```

### Tips for Large Datasets with Standard Pagination

**Stream results** — don't wait for all data before processing. Use generators (as shown above) to handle results incrementally:

```python
for review in paginator.paginate("appstore/reviews", params={"appId": "389801252"}):
    process_review(review)  # Handle immediately
```

**Checkpoint and resume** — for long-running jobs, save the last cursor. If interrupted, resume from it instead of restarting:

```python
import json, os

checkpoint_file = "pagination_checkpoint.json"

last_cursor = None
if os.path.exists(checkpoint_file):
    with open(checkpoint_file) as f:
        last_cursor = json.load(f).get("cursor")

if last_cursor:
    params = {"appId": "389801252", "cursor": last_cursor}
else:
    params = {"appId": "389801252"}

for i, review in enumerate(paginator.paginate("appstore/reviews", params=params)):
    process_review(review)
    if i % 100 == 0:
        # Checkpointing cursor requires reading it from response — see full implementation
        pass
```

**Parallel fetching** — if you have multiple IDs, fetch them concurrently:

```python
from concurrent.futures import ThreadPoolExecutor

app_ids = ["389801252", "544007664", "310633997"]

def fetch_reviews(app_id):
    return list(paginator.paginate(
        "appstore/reviews",
        params={"appId": app_id},
    ))

with ThreadPoolExecutor(max_workers=4) as executor:
    results = list(executor.map(fetch_reviews, app_ids))
```

### When to Use Standard Pagination

* **Fine-grained control**: You want to process data as it arrives
* **Subset of data**: You need only the first N results, not everything
* **Cost efficiency**: You want to stop early and avoid fetching unneeded data
* **Real-time processing**: You're streaming results to another system

## Managed Pagination (Pullbay Controls the Flow)

Managed pagination simplifies everything: pass `maxItems` in a single request, and Pullbay fetches all the pages internally and returns them in one response. Perfect for automation tools and batch jobs.

### How It Works

1. Make a single request with `maxItems=<limit>` set to the total number of records you want
2. Pullbay fetches data across as many internal pages as needed
3. Receive a single response containing all matching items up to `maxItems`
4. No cursor handling or loop logic needed on your end

`maxItems` and `cursor` are **mutually exclusive** — you cannot combine them. The maximum value of `maxItems` varies by endpoint (see the endpoint reference for each service).

### Example: Managed Pagination with cURL

```bash
curl -X GET "https://api.pullbay.com/appstore/reviews" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -G \
  --data-urlencode "appId=389801252" \
  --data-urlencode "country=us" \
  --data-urlencode "maxItems=500"
```

**Response (all up to 500 items in one response):**

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    { "id": "rev_001", "text": "Great service!", "rating": 5 },
    { "id": "rev_002", "text": "Good experience", "rating": 4 }
  ],
  "pagination": {
    "page": null,
    "hasNextPage": false,
    "cursor": null,
    "offset": null
  },
  "pricing": { "creditsCharged": 503 }
}
```

`hasNextPage` will be `false` because the response already contains everything up to `maxItems`.

### Complete Python Implementation

```python
import requests
from typing import List, Dict, Any

class PullbayManagedPaginator:
    def __init__(self, api_key, base_url="https://api.pullbay.com"):
        self.api_key = api_key
        self.base_url = base_url
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {api_key}",
        })

    def fetch_all(self, endpoint, params=None, max_items=200, timeout=120) -> List[Dict[str, Any]]:
        """
        Fetch up to max_items results in a single request using managed pagination.

        Args:
            endpoint: API endpoint e.g. 'appstore/reviews'
            params: Query parameters dict
            max_items: Maximum items to fetch (bounded by endpoint limit)
            timeout: HTTP timeout in seconds

        Returns:
            List of all returned items
        """
        if params is None:
            params = {}

        params = dict(params)
        params["maxItems"] = max_items

        try:
            response = self.session.get(
                f"{self.base_url}/{endpoint}",
                params=params,
                timeout=timeout,
            )
            response.raise_for_status()
        except requests.exceptions.RequestException as e:
            print(f"Failed to fetch all results: {e}")
            raise

        body = response.json()
        print(f"requestId: {body['requestId']}, creditsCharged: {body['pricing']['creditsCharged']}")
        return body.get("data", [])

# Usage
managed = PullbayManagedPaginator(api_key="your_api_key")

all_reviews = managed.fetch_all(
    endpoint="appstore/reviews",
    params={"appId": "389801252", "country": "us"},
    max_items=500,
)

print(f"Total reviews: {len(all_reviews)}")
for review in all_reviews:
    print(f"Review {review['id']}: rating {review.get('rating')}")
```

### When to Use Managed Pagination

* **Automation tools**: Using n8n, Make.com, Zapier, or similar platforms
* **Complete datasets**: You need all matching records up to the endpoint's limit
* **Simplicity**: You prefer one request over pagination loops
* **Batch jobs**: Running scheduled exports or data synchronization

### Important: Timeout Configuration

Managed pagination fetches all pages internally, which takes longer than a single page. Set your HTTP client timeout accordingly:

* **Standard pagination** (one page): 30 seconds is sufficient
* **Managed pagination** (many internal pages): 60–120 seconds recommended

```python
# Correct: generous timeout for managed
reviews = managed.fetch_all(
    "appstore/reviews",
    params={"appId": "389801252"},
    max_items=500,
    timeout=120,
)

# Risky: default 30s may not be enough for large maxItems
reviews = managed.fetch_all(
    "appstore/reviews",
    params={"appId": "389801252"},
    max_items=500,
    timeout=30,  # Too short for large datasets
)
```

## Choosing the Right Pagination Model

| Scenario                          | Best Choice | Reason                                  |
| --------------------------------- | ----------- | --------------------------------------- |
| Building a custom application     | Standard    | Fine-grained control over data flow     |
| Using n8n, Make.com, Zapier       | Managed     | Platforms prefer single requests        |
| Need first 100 results only       | Standard    | Avoid fetching unnecessary data         |
| Exporting all data monthly        | Managed     | Simplicity; no pagination loop needed   |
| Real-time data processing         | Standard    | Process results as they arrive          |
| Reducing credit usage             | Standard    | Fetch only what you need, stop early    |
| Large dataset (near endpoint max) | Standard    | Managed may timeout for very large sets |
| Simplest possible implementation  | Managed     | Single API call vs. pagination loop     |
| Scheduled batch job               | Managed     | Fire-and-forget simplicity              |

## Performance Comparison

| Metric                  | Standard Pagination             | Managed Pagination                      |
| ----------------------- | ------------------------------- | --------------------------------------- |
| Time to first result    | 1–2 seconds                     | Longer (Pullbay fetches all internally) |
| Total time for all data | Gradual (per page)              | All at once                             |
| Memory usage            | Low (process incrementally)     | Higher (full result in one response)    |
| Code complexity         | Higher (cursor loop)            | Low (single request)                    |
| Partial fetching        | Yes (stop when you have enough) | No (fetches up to `maxItems`)           |
| Best for                | Custom apps, cost optimization  | Automation tools, exports               |
| Failure recovery        | Easy (resume from last cursor)  | Retry entire request                    |

## Credit Costs

**The credit cost is identical for both models when fetching the same number of items.**

Credits are always charged as: `base_cost + (items_returned × 1)`.

If you fetch 500 App Store reviews for one app:

* **Standard**: multiple requests totalling 500 items → same total credits
* **Managed** with `maxItems=500`: one request, 500 items → same total credits

The difference is **control**, not cost:

* **Standard**: You decide how many items to fetch and can stop early
* **Managed**: Pullbay fetches up to `maxItems`; you pay for everything returned

To minimize costs with Managed pagination, use query filters to reduce the dataset before requesting:

```python
# Expensive: fetch all reviews regardless of sort
all_reviews = managed.fetch_all("appstore/reviews", {"appId": "389801252"}, max_items=500)

# Cheaper: fetch only what you need with targeted filters
recent_reviews = managed.fetch_all(
    "appstore/reviews",
    {"appId": "389801252", "sort": "recent"},
    max_items=50,
)
```

## Caching Strategies

Combine pagination with caching to maximize efficiency and reduce credit usage.

### Standard Pagination Caching

Cache results by cursor key to avoid re-fetching pages:

```python
import hashlib, json
from datetime import datetime, timedelta

class CachedPaginator:
    def __init__(self, api_key, cache_ttl=3600):
        self.paginator = PullbayPaginator(api_key)
        self.cache = {}
        self.cache_ttl = cache_ttl

    def paginate_cached(self, endpoint, params):
        cursor = None

        while True:
            cache_key = self._make_key(endpoint, params, cursor)

            if cache_key in self.cache:
                entry = self.cache[cache_key]
                if datetime.now() < entry["expires"]:
                    items, next_cursor = entry["items"], entry["next_cursor"]
                else:
                    del self.cache[cache_key]
                    items, next_cursor = self._fetch_page(endpoint, params, cursor)
                    self._cache(cache_key, items, next_cursor)
            else:
                items, next_cursor = self._fetch_page(endpoint, params, cursor)
                self._cache(cache_key, items, next_cursor)

            yield from items

            if not next_cursor:
                break
            cursor = next_cursor

    def _fetch_page(self, endpoint, params, cursor):
        req_params = dict(params)
        if cursor:
            req_params["cursor"] = cursor
        resp = self.paginator.session.get(
            f"{self.paginator.base_url}/{endpoint}", params=req_params
        )
        resp.raise_for_status()
        body = resp.json()
        pagination = body.get("pagination", {})
        return body.get("data", []), pagination.get("cursor") if pagination.get("hasNextPage") else None

    def _cache(self, key, items, next_cursor):
        self.cache[key] = {
            "items": items,
            "next_cursor": next_cursor,
            "expires": datetime.now() + timedelta(seconds=self.cache_ttl),
        }

    def _make_key(self, endpoint, params, cursor):
        return hashlib.md5(
            json.dumps({"endpoint": endpoint, "params": params, "cursor": cursor}, sort_keys=True).encode()
        ).hexdigest()
```

### Managed Pagination Caching

Cache the entire result as a complete dataset:

```python
class CachedManagedPaginator:
    def __init__(self, api_key, cache_ttl=3600):
        self.managed = PullbayManagedPaginator(api_key)
        self.cache = {}
        self.cache_ttl = cache_ttl

    def fetch_all_cached(self, endpoint, params, max_items=200):
        key = hashlib.md5(
            json.dumps({"endpoint": endpoint, "params": params, "max_items": max_items}, sort_keys=True).encode()
        ).hexdigest()

        if key in self.cache:
            entry = self.cache[key]
            if datetime.now() < entry["expires"]:
                return entry["data"]
            del self.cache[key]

        data = self.managed.fetch_all(endpoint, params, max_items=max_items)
        self.cache[key] = {"data": data, "expires": datetime.now() + timedelta(seconds=self.cache_ttl)}
        return data
```

### Cache TTL Guidelines

| Data Type           | Freshness | Recommended TTL |
| ------------------- | --------- | --------------- |
| Real-time metrics   | Minutes   | 5 minutes       |
| Daily reports       | Hours     | 6 hours         |
| Historical reviews  | Days      | 24 hours        |
| Static app metadata | Weeks     | 7 days          |

## Frequently Asked Questions

<details>

<summary>Are cursor strings safe to store?</summary>

Yes, but treat them as temporary tokens. Cursors are opaque (you cannot modify them), may expire after some time, and are tied to the specific query that generated them. You can safely store them in a database for resuming interrupted jobs.

</details>

<details>

<summary>Do cursors expire?</summary>

Yes. If you resume from an expired cursor, the API will return an error. Handle this by catching the error and restarting from the beginning:

```python
try:
    results = list(paginator.paginate(endpoint, {**params, "cursor": saved_cursor}))
except Exception as e:
    if "cursor" in str(e).lower():
        print("Cursor expired — restarting from beginning")
        results = list(paginator.paginate(endpoint, params))
    else:
        raise
```

</details>

<details>

<summary>What is the difference in credit cost between standard and managed pagination?</summary>

There is no difference for the same number of items fetched. Both charge `base_cost + (items × 1)`. The practical difference is that with Standard you can stop early (and pay less), while Managed always fetches up to `maxItems`.

</details>

<details>

<summary>Can I use managed pagination in n8n?</summary>

Yes. Set `maxItems` as a query parameter in your HTTP Request node. n8n will execute the single request, receive all results in one response, and process them — no pagination loop needed in your workflow.

</details>

<details>

<summary>What happens if `maxItems` exceeds the endpoint's limit?</summary>

The API caps the result at the endpoint's maximum (e.g. 500 for App Store reviews). Requesting `maxItems=9999` on that endpoint returns at most 500 items. Check the endpoint reference for each service's maximum.

</details>

<details>

<summary>Can I combine `cursor` and `maxItems`?</summary>

No. They are mutually exclusive. Providing both will result in a `400 VALIDATION_ERROR`.

</details>

## Summary

**Choose Standard pagination for:**

* Custom control over data flow
* Fetching subsets (first N results)
* Cost optimization (stop early)
* Processing results incrementally

**Choose Managed pagination for:**

* Automation tools (n8n, Make.com, Zapier)
* Complete dataset exports up to the endpoint limit
* Simplicity (one request)
* Batch jobs with sufficient timeout

Both models charge the same per item retrieved. The difference is flexibility and control. Combine either with caching for maximum efficiency and cost savings.


# API Integration Guide

This guide covers integrating Pullbay's data APIs into backend applications and services. Learn authentication, error handling, rate limiting, and how to build robust integrations across Python, Node.js, PHP, and Ruby.

***

## Architecture: Server-Side Integration Only

{% hint style="danger" %}
**Critical Security Rule**: Never expose your Pullbay API key in client-side code.
{% endhint %}

### Why Server-Side Only?

API keys are secrets. Exposing them in client-side JavaScript, mobile apps, or public repositories allows attackers to:

* Consume your API quota and credits
* Impersonate your application
* Access your data
* Cause financial damage through runaway API usage

### Where to Store Your API Key

**Do:**

* Environment variables in your backend application
* Secrets manager (AWS Secrets Manager, HashiCorp Vault, Google Secret Manager)
* Encrypted configuration files (accessed only by your backend)
* Secure CI/CD pipeline secrets

**Don't:**

* Hardcoded in source files
* Client-side JavaScript
* Mobile app code
* Public repositories (GitHub, GitLab, etc.)
* Unencrypted configuration files

***

## Client-Side Access: Build a Backend Proxy

If your frontend needs to fetch Pullbay data, **never call Pullbay directly from the browser**. Instead, build a backend API endpoint that acts as a proxy:

```
Frontend → Your Backend Proxy → Pullbay API
```

### Proxy Pattern

```
GET /api/reviews?appId=389801252
    ↓
[Your Backend Server]
    ↓ (API Key added securely)
Pullbay API
    ↓
[Your Backend Server]
    ↓
Frontend (JSON response)
```

**Benefits:**

* API key never leaves your backend
* Control who can access review data
* Add authentication/authorization
* Rate limit per user
* Cache responses to reduce credits

**Example Backend Proxy (Node.js):**

```javascript
app.get('/api/reviews', authMiddleware, async (req, res) => {
  const { appId } = req.query;

  const response = await axios.get('https://api.pullbay.com/appstore/reviews', {
    params: { appId },
    headers: { 'Authorization': `Bearer ${process.env.PULLBAY_API_KEY}` }
  });

  res.json(response.data);
});
```

***

## Response Envelope

All Pullbay responses follow the same envelope. Understanding this structure is essential before implementing any client:

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [...],
  "pagination": {
    "page": null,
    "hasNextPage": true,
    "cursor": "eyJpZCI6InJldl8wNTAifQ==",
    "offset": null
  },
  "pricing": { "creditsCharged": 52 }
}
```

| Field                    | Description                                                                                      |
| ------------------------ | ------------------------------------------------------------------------------------------------ |
| `requestId`              | Unique request identifier — log this for every call. Also returned as the `X-Request-Id` header. |
| `status`                 | HTTP status code mirrored in the body                                                            |
| `success`                | `true` on success, `false` on error                                                              |
| `data`                   | Array of result objects                                                                          |
| `pagination`             | Present on paginated endpoints; fields are `null` when not applicable                            |
| `pricing.creditsCharged` | Credits consumed by this request                                                                 |

Error responses:

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 400,
  "message": "Validation failed",
  "success": false,
  "error": { "code": "VALIDATION_ERROR" }
}
```

***

## Centralized PullbayClient Class

Create a centralized client class for all Pullbay API calls. This ensures consistent authentication, error handling, and credit tracking across your application.

### Python Example

```python
import os
import requests
from urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter

class PullbayClient:
    """Centralized client for Pullbay API calls."""

    BASE_URL = "https://api.pullbay.com"

    def __init__(self, api_key=None):
        self.api_key = api_key or os.getenv("PULLBAY_API_KEY")
        if not self.api_key:
            raise ValueError("PULLBAY_API_KEY environment variable not set")

        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {self.api_key}",
        })

        retry_strategy = Retry(
            total=3,
            status_forcelist=[429, 500, 502, 503, 504],
            allowed_methods=["GET"],
            backoff_factor=1,
        )
        adapter = HTTPAdapter(max_retries=retry_strategy)
        self.session.mount("https://", adapter)

    def get_appstore_reviews(self, app_id, country="us", sort="recent", max_items=None):
        """
        Fetch App Store reviews for an app.

        Args:
            app_id (str): App Store numeric ID matching ^\d{8,12}$
            country (str): 2-letter country code (default: 'us')
            sort (str): 'recent' or 'helpful' (default: 'recent')
            max_items (int): Bulk pull limit 1–500; omit to page manually with cursor

        Returns:
            dict: Full Pullbay response envelope
        """
        params = {"appId": app_id, "country": country, "sort": sort}
        if max_items is not None:
            params["maxItems"] = max_items

        response = self.session.get(
            f"{self.BASE_URL}/appstore/reviews",
            params=params,
            timeout=120,
        )
        response.raise_for_status()
        return response.json()


# Usage
client = PullbayClient()
result = client.get_appstore_reviews(app_id="389801252", country="us", max_items=50)
print(f"Fetched {len(result['data'])} reviews — {result['pricing']['creditsCharged']} credits")
```

***

## Implementation Checklist

Before deploying your integration, ensure you've implemented:

* [ ] **Authentication**: API key stored in environment variables or secrets manager
* [ ] **Error Handling**: Catch and handle 4xx and 5xx errors appropriately
* [ ] **Rate Limiting**: Respect rate limits; implement exponential backoff on 429
* [ ] **Credit Tracking**: Log `pricing.creditsCharged` from every response
* [ ] **Request Logging**: Log `requestId` for every API call for debugging
* [ ] **Retries**: Retry on 429 and 5xx with backoff; never retry 4xx except 429
* [ ] **Timeout**: Set 30s timeout for single-page requests; 120s when using `maxItems`
* [ ] **Monitoring**: Set up alerts for API failures and credit burn rate
* [ ] **Testing**: Mock the Pullbay response envelope in unit tests; never call the real API in tests

***

## Language-Specific Examples

{% tabs %}
{% tab title="Python" %}

### Python (with Requests and Retries)

Complete production-ready Python integration:

```python
import os
import logging
import requests
from urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class PullbayClient:
    """Production-grade Pullbay API client."""

    BASE_URL = "https://api.pullbay.com"

    def __init__(self, api_key=None, max_retries=3):
        self.api_key = api_key or os.getenv("PULLBAY_API_KEY")
        if not self.api_key:
            raise ValueError("PULLBAY_API_KEY not configured")

        self.credits_used = 0
        self.session = self._build_session(max_retries)

    def _build_session(self, max_retries):
        session = requests.Session()
        session.headers.update({"Authorization": f"Bearer {self.api_key}"})

        retry = Retry(
            total=max_retries,
            status_forcelist=[429, 500, 502, 503, 504],
            allowed_methods=["GET"],
            backoff_factor=1,
            raise_on_status=False,
        )
        adapter = HTTPAdapter(max_retries=retry)
        session.mount("https://", adapter)
        return session

    def get_appstore_reviews(self, app_id, country="us", sort="recent", max_items=None):
        """
        Fetch App Store reviews.

        Args:
            app_id (str): App Store ID — digits only, 8–12 chars
            country (str): 2-letter country code (default: 'us')
            sort (str): 'recent' or 'helpful' (default: 'recent')
            max_items (int): 1–500. Omit to paginate manually with cursor.

        Returns:
            dict: Pullbay response envelope
        """
        params = {"appId": app_id, "country": country, "sort": sort}
        if max_items is not None:
            params["maxItems"] = max_items

        try:
            logger.info("Fetching App Store reviews for app %s", app_id)

            response = self.session.get(
                f"{self.BASE_URL}/appstore/reviews",
                params=params,
                timeout=120,
            )

            if response.status_code == 401:
                raise ValueError("Invalid API key")
            if response.status_code == 402:
                raise RuntimeError("Insufficient credits — add credits in your dashboard")
            if response.status_code == 429:
                raise RuntimeError("Rate limited — back off and retry")
            if response.status_code >= 500:
                raise RuntimeError(f"Pullbay server error: {response.status_code}")

            response.raise_for_status()

            body = response.json()
            credits = body["pricing"]["creditsCharged"]
            self.credits_used += credits
            logger.info(
                "Fetched %d reviews (requestId=%s, creditsCharged=%d)",
                len(body["data"]), body["requestId"], credits,
            )
            return body

        except requests.exceptions.Timeout:
            logger.error("Request timed out")
            raise
        except requests.exceptions.ConnectionError:
            logger.error("Connection error — unable to reach Pullbay API")
            raise

    def paginate_appstore_reviews(self, app_id, country="us", sort="recent", max_results=None):
        """
        Generator that yields individual reviews across all cursor pages.

        Args:
            app_id (str): App Store ID
            country (str): 2-letter country code
            sort (str): 'recent' or 'helpful'
            max_results (int): Stop after this many results (None = all)
        """
        cursor = None
        fetched = 0

        while True:
            params = {"appId": app_id, "country": country, "sort": sort}
            if cursor:
                params["cursor"] = cursor

            response = self.session.get(
                f"{self.BASE_URL}/appstore/reviews",
                params=params,
                timeout=30,
            )
            response.raise_for_status()
            body = response.json()

            for item in body["data"]:
                yield item
                fetched += 1
                if max_results and fetched >= max_results:
                    return

            pagination = body.get("pagination", {})
            if not pagination.get("hasNextPage"):
                break
            cursor = pagination.get("cursor")
            if not cursor:
                break


# Usage
if __name__ == "__main__":
    client = PullbayClient()

    # Managed: fetch up to 500 reviews in one call
    result = client.get_appstore_reviews(
        app_id="389801252",
        country="us",
        sort="recent",
        max_items=100,
    )
    print(f"Total: {len(result['data'])}, Credits: {result['pricing']['creditsCharged']}")

    # Standard: iterate one page at a time
    for review in client.paginate_appstore_reviews("389801252", max_results=50):
        print(f"{review.get('userName', '—')}: {review.get('score', '—')}⭐")
```

{% endtab %}

{% tab title="Node.js" %}

### Node.js (with Axios)

```javascript
const axios = require('axios');

class PullbayClient {
  constructor(apiKey = null) {
    this.apiKey = apiKey || process.env.PULLBAY_API_KEY;
    if (!this.apiKey) {
      throw new Error('PULLBAY_API_KEY environment variable not set');
    }

    this.baseURL = 'https://api.pullbay.com';
    this.creditsUsed = 0;

    this.client = axios.create({
      baseURL: this.baseURL,
      timeout: 120000,
      headers: { 'Authorization': `Bearer ${this.apiKey}` },
    });

    this.client.interceptors.response.use(null, this._retryInterceptor.bind(this));
  }

  async getAppstoreReviews(appId, options = {}) {
    const params = {
      appId,
      country: options.country || 'us',
      sort: options.sort || 'recent',
    };

    if (options.maxItems) {
      params.maxItems = options.maxItems;
    }

    try {
      const response = await this.client.get('/appstore/reviews', { params });
      const body = response.data;

      this.creditsUsed += body.pricing.creditsCharged;
      console.log(
        `Fetched ${body.data.length} reviews — requestId: ${body.requestId}, credits: ${body.pricing.creditsCharged}`
      );

      return body;
    } catch (error) {
      const status = error.response?.status;
      if (status === 401) throw new Error('Invalid API key');
      if (status === 402) throw new Error('Insufficient credits');
      if (status === 429) throw new Error('Rate limited — retry after backoff');
      if (status >= 500) throw new Error(`Pullbay server error: ${status}`);
      throw error;
    }
  }

  async *paginateAppstoreReviews(appId, options = {}) {
    let cursor = null;
    let fetched = 0;
    const maxResults = options.maxResults || Infinity;

    while (true) {
      const params = {
        appId,
        country: options.country || 'us',
        sort: options.sort || 'recent',
      };
      if (cursor) params.cursor = cursor;

      const response = await this.client.get('/appstore/reviews', { params });
      const body = response.data;

      for (const item of body.data) {
        yield item;
        fetched++;
        if (fetched >= maxResults) return;
      }

      if (!body.pagination?.hasNextPage) break;
      cursor = body.pagination?.cursor;
      if (!cursor) break;
    }
  }

  async _retryInterceptor(error) {
    const config = error.config;
    if (!config) return Promise.reject(error);

    config.retryCount = config.retryCount || 0;

    const isRetryable =
      (error.response?.status >= 500) ||
      ['ECONNABORTED', 'ECONNRESET'].includes(error.code);

    if (isRetryable && config.retryCount < 3) {
      config.retryCount++;
      const delay = Math.pow(2, config.retryCount - 1) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
      return this.client(config);
    }

    return Promise.reject(error);
  }
}

// Usage
(async () => {
  const client = new PullbayClient();

  // Managed: fetch up to 100 reviews in one call
  const result = await client.getAppstoreReviews('389801252', {
    country: 'us',
    sort: 'recent',
    maxItems: 100,
  });
  console.log(`Total: ${result.data.length}, Credits: ${result.pricing.creditsCharged}`);

  // Standard: iterate one page at a time
  for await (const review of client.paginateAppstoreReviews('389801252', { maxResults: 50 })) {
    console.log(`${review.userName}: ${review.score}⭐`);
  }
})();

module.exports = PullbayClient;
```

{% endtab %}

{% tab title="PHP" %}

### PHP (with Guzzle)

```php
<?php

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Message\ResponseInterface;

class PullbayClient {
    private Client $httpClient;
    private string $apiKey;
    private int $creditsUsed = 0;

    public function __construct(?string $apiKey = null) {
        $this->apiKey = $apiKey ?: (string) getenv('PULLBAY_API_KEY');

        if (!$this->apiKey) {
            throw new \Exception('PULLBAY_API_KEY environment variable not set');
        }

        $stack = HandlerStack::create();
        $stack->push(Middleware::retry($this->retryDecider(), $this->retryDelay()));

        $this->httpClient = new Client([
            'base_uri' => 'https://api.pullbay.com/',
            'handler'  => $stack,
            'timeout'  => 120,
            'headers'  => [
                'Authorization' => 'Bearer ' . $this->apiKey,
            ],
        ]);
    }

    public function getAppstoreReviews(string $appId, array $options = []): array {
        $query = [
            'appId'   => $appId,
            'country' => $options['country'] ?? 'us',
            'sort'    => $options['sort'] ?? 'recent',
        ];

        if (isset($options['maxItems'])) {
            $query['maxItems'] = $options['maxItems'];
        }

        $response = $this->httpClient->get('appstore/reviews', ['query' => $query]);
        $body = json_decode((string) $response->getBody(), true);

        $this->creditsUsed += $body['pricing']['creditsCharged'];
        echo "Fetched " . count($body['data']) . " reviews"
            . " (requestId: {$body['requestId']},"
            . " credits: {$body['pricing']['creditsCharged']})\n";

        return $body;
    }

    private function retryDecider(): callable {
        return function (int $retries, $request, ?ResponseInterface $response = null, $error = null): bool {
            if ($retries >= 3) return false;
            if ($response && $response->getStatusCode() >= 500) return true;
            if ($error !== null) return true;
            return false;
        };
    }

    private function retryDelay(): callable {
        return fn(int $retries): int => 1000 * (int) pow(2, $retries);
    }

    public function getCreditsUsed(): int {
        return $this->creditsUsed;
    }
}

// Usage
$client = new PullbayClient();

$result = $client->getAppstoreReviews('389801252', [
    'country'  => 'us',
    'sort'     => 'recent',
    'maxItems' => 100,
]);

echo "Total: " . count($result['data']) . "\n";
echo "Credits: " . $result['pricing']['creditsCharged'] . "\n";

foreach ($result['data'] as $review) {
    echo "{$review['userName']} ({$review['score']}⭐): "
        . substr($review['text'] ?? '', 0, 80) . "\n";
}
```

{% endtab %}

{% tab title="Ruby" %}

### Ruby (with Net::HTTP)

```ruby
require 'net/http'
require 'json'
require 'uri'

class PullbayClient
  BASE_URL = 'https://api.pullbay.com'.freeze

  def initialize(api_key = nil)
    @api_key = api_key || ENV['PULLBAY_API_KEY']
    raise 'PULLBAY_API_KEY not set' unless @api_key

    @credits_used = 0
    @max_retries = 3
  end

  def get_appstore_reviews(app_id, country: 'us', sort: 'recent', max_items: nil)
    params = { appId: app_id, country: country, sort: sort }
    params[:maxItems] = max_items if max_items

    body = request(path: '/appstore/reviews', params: params)

    @credits_used += body['pricing']['creditsCharged']
    puts "Fetched #{body['data'].length} reviews " \
         "(requestId: #{body['requestId']}, credits: #{body['pricing']['creditsCharged']})"

    body
  end

  def paginate_appstore_reviews(app_id, country: 'us', sort: 'recent', max_results: nil)
    return enum_for(:paginate_appstore_reviews, app_id, country: country, sort: sort, max_results: max_results) unless block_given?

    cursor  = nil
    fetched = 0

    loop do
      params = { appId: app_id, country: country, sort: sort }
      params[:cursor] = cursor if cursor

      body       = request(path: '/appstore/reviews', params: params)
      pagination = body['pagination'] || {}

      body['data'].each do |item|
        yield item
        fetched += 1
        return if max_results && fetched >= max_results
      end

      break unless pagination['hasNextPage']
      cursor = pagination['cursor']
      break unless cursor
    end
  end

  private

  def request(path:, params: {})
    retries = 0

    begin
      uri       = URI("#{BASE_URL}#{path}")
      uri.query = URI.encode_www_form(params)

      http          = Net::HTTP.new(uri.host, uri.port)
      http.use_ssl  = true
      http.read_timeout = 120

      req = Net::HTTP::Get.new(uri)
      req['Authorization'] = "Bearer #{@api_key}"

      response = http.request(req)

      case response.code.to_i
      when 200..299
        JSON.parse(response.body)
      when 401
        raise 'Invalid API key'
      when 402
        raise 'Insufficient credits'
      when 429
        raise 'Rate limited'
      when 500..599
        raise "Server error: #{response.code}"
      else
        raise "HTTP #{response.code}"
      end

    rescue RuntimeError => e
      retries += 1
      if retries <= @max_retries && e.message.start_with?('Server error', 'Rate limited')
        sleep(2**retries)
        retry
      end
      raise
    end
  end
end

# Usage
client = PullbayClient.new

result = client.get_appstore_reviews('389801252', country: 'us', sort: 'recent', max_items: 100)
puts "Total: #{result['data'].length}, Credits: #{result['pricing']['creditsCharged']}"

client.paginate_appstore_reviews('389801252', max_results: 50) do |review|
  puts "#{review['userName']}: #{review['score']}⭐"
end
```

{% endtab %}
{% endtabs %}

***

## Testing and Mocking

### Mocking Responses in Unit Tests

Never make real API calls in unit tests. Mock the Pullbay response envelope instead.

**Python mock example:**

```python
import unittest
from unittest.mock import patch, MagicMock
from my_app import PullbayClient

class TestPullbayIntegration(unittest.TestCase):

    @patch.object(PullbayClient, 'session')
    def test_get_appstore_reviews(self, mock_session):
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            'requestId': '3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24',
            'status': 200,
            'message': 'OK',
            'success': True,
            'data': [
                {
                    'id': 'rev_001',
                    'userName': 'test_user',
                    'score': 5,
                    'title': 'Great app!',
                    'text': 'Loved this app',
                }
            ],
            'pagination': {
                'page': None,
                'hasNextPage': False,
                'cursor': None,
                'offset': None,
            },
            'pricing': {'creditsCharged': 2},
        }
        mock_session.get.return_value = mock_response

        client = PullbayClient(api_key='test_key')
        result = client.get_appstore_reviews(app_id='389801252')

        self.assertEqual(len(result['data']), 1)
        self.assertEqual(result['data'][0]['userName'], 'test_user')
        self.assertEqual(result['pricing']['creditsCharged'], 2)
        self.assertIn('requestId', result)

if __name__ == '__main__':
    unittest.main()
```

***

## Security Considerations

### Environment Variables vs Secrets Manager

**For development** — use `.env` files:

```bash
PULLBAY_API_KEY=your_api_key_here
```

```python
import os
api_key = os.getenv('PULLBAY_API_KEY')
```

**For production** — use a secrets manager:

```python
import boto3

def get_api_key():
    client = boto3.client('secretsmanager', region_name='us-east-1')
    response = client.get_secret_value(SecretId='pullbay-api-key')
    return response['SecretString']
```

### API Key Rotation

1. Generate a new API key in the [Pullbay dashboard](https://dashboard.pullbay.com/dashboard)
2. Update your secrets manager with the new key
3. Redeploy or reload configuration
4. Verify the new key works
5. Deactivate the old key

***

## Frequently Asked Questions

<details>

<summary>Should I cache Pullbay responses?</summary>

Yes. Caching reduces credits and improves latency. Cache the full response body keyed by endpoint + parameters. Use a TTL appropriate for data freshness (e.g. 1 hour for reviews, 24 hours for static app metadata).

```python
from datetime import datetime, timedelta

class CachedPullbayClient(PullbayClient):
    def __init__(self, *args, cache_ttl=3600, **kwargs):
        super().__init__(*args, **kwargs)
        self._cache = {}
        self._cache_ttl = cache_ttl

    def get_appstore_reviews(self, app_id, **kwargs):
        key = f"{app_id}:{kwargs}"
        entry = self._cache.get(key)
        if entry and datetime.now() < entry['expires']:
            return entry['data']

        data = super().get_appstore_reviews(app_id, **kwargs)
        self._cache[key] = {
            'data': data,
            'expires': datetime.now() + timedelta(seconds=self._cache_ttl),
        }
        return data
```

</details>

<details>

<summary>How do I handle API key rotation without downtime?</summary>

Store the key in a secrets manager rather than in code. When you rotate:

1. Update the secret in the secrets manager
2. The application reads the new key on the next request (or on restart, depending on your setup)
3. No code changes or redeployment needed

</details>

<details>

<summary>Can I call Pullbay from a browser?</summary>

No. Never expose your API key in client-side code. Build a backend proxy endpoint that adds the key server-side (see the Proxy Pattern section above).

</details>

<details>

<summary>What is the difference between <code>maxItems</code> and using <code>cursor</code>?</summary>

* `maxItems` — Pullbay paginates internally and returns all items in a single response (up to the endpoint maximum). Simpler but takes longer for large datasets.
* `cursor` — You paginate one page at a time, processing results incrementally. More control; you can stop early to save credits.

They are mutually exclusive — you cannot use both on the same request.

</details>

<details>

<summary>How do I monitor credit usage?</summary>

Log `pricing.creditsCharged` from every response. Accumulate totals in your application and set up alerts when burn rate exceeds a threshold. The Pullbay dashboard also shows usage analytics.

</details>

<details>

<summary>What's the best way to handle rate limiting?</summary>

Respond to `429` responses by reading the `X-RateLimit-Reset` header and waiting until that timestamp before retrying. The Python, Node.js, PHP, and Ruby examples above all implement exponential backoff as a fallback.

</details>


# Pullbay x Make Integration

Make.com (formerly Integromat) is a visual automation platform that lets you connect apps and APIs without code. This guide shows you how to use Pullbay's App Store Reviews API within Make scenarios—today with the HTTP module, and with a native Pullbay module coming soon.

## What You Can Build with Pullbay and Make

Make + Pullbay enables you to:

* **Automate daily review collection**: Pull fresh reviews on a schedule and route them automatically
* **Filter and alert on critical reviews**: Catch 1–2 star reviews and send instant notifications
* **Log reviews to Google Sheets**: Build a live spreadsheet of all customer feedback
* **Send Slack notifications**: Post reviews to team channels in real-time
* **Feed reviews to Airtable**: Create structured databases for analysis and collaboration
* **Update CRM records**: Sync reviews to Pipedrive, HubSpot, or other CRM systems
* **Create email digests**: Send summaries of high-priority reviews to stakeholders

## How the Integration Works

Make uses a visual, modular approach. You'll chain together modules—a scheduler, an HTTP request module to call Pullbay, a JSON parser, filters, and actions. No native Pullbay module exists yet, but the generic HTTP module gives you full API access today. A native Pullbay module is coming soon.

{% stepper %}
{% step %}

### Create a New Scenario

1. Log in to [make.com](https://make.com/)
2. Click **Create a new scenario**
3. Choose a name (e.g., "Pullbay Daily Reviews to Slack")
4. You'll see the scenario builder with a blank canvas
   {% endstep %}

{% step %}

### Add a Schedule Trigger

1. Click **Add a module** (or the + icon)
2. Search for **Scheduled** (the built-in scheduler)
3. Select **Schedule > Trigger**
4. Configure the schedule:
   * **Type**: Choose "Daily," "Weekly," or "Monthly"
   * **Repeat every**: Set to 1 (runs daily)
   * **Start time**: Choose a time (e.g., 9 AM)
   * **Interval**: Daily (or your preference)
5. Click **OK**

Your scenario now runs on schedule.
{% endstep %}

{% step %}

### Add an HTTP Module to Fetch Reviews

1. Click the **+** icon below the Schedule module
2. Search for **HTTP** and select **HTTP > Make a request**
3. Configure the HTTP module with these settings:

   | Setting            | Value                                              |
   | ------------------ | -------------------------------------------------- |
   | **URL**            | `https://api.pullbay.com/v1/app-store/reviews/all` |
   | **Method**         | GET                                                |
   | **Headers**        | Authorization: Bearer YOUR\_API\_KEY               |
   | **Query string**   | app\_id=YOUR\_APP\_ID                              |
   | **Parse response** | Yes (checked)                                      |

Here's what each field does:

* **URL**: Points to Pullbay's endpoint that returns all reviews for your app
* **Method**: GET because we're retrieving data (not creating or updating)
* **Headers**: Contains your API authentication. Replace `YOUR_API_KEY` with your actual Pullbay API key
* **Query string**: Tells Pullbay which app to fetch reviews for. Replace `YOUR_APP_ID` with your actual App Store app ID
* **Parse response**: Tells Make to parse the JSON response automatically

4. Click **OK**

Make will test the request and show you the response structure—a JSON object with a `data` array containing review objects.
{% endstep %}

{% step %}

### Parse the Response (Optional but Recommended)

To make the response easier to work with, add a JSON parser:

1. Click **+** below the HTTP module
2. Search for **JSON** and select **JSON > Parse JSON**
3. In the **JSON string** field, select the response body from the HTTP module (the field should show a dropdown with output from the HTTP step)
4. Click **OK**

This creates a clean data structure you can map to the next steps.
{% endstep %}

{% step %}

### Filter Reviews (Optional)

To act only on critical reviews, add a filter:

1. Click **+** below the JSON parser
2. Search for **Router** or **Filter**
3. Select **Flow control > Router** (or **Filter**)
4. Set up a condition:
   * **Field**: `rating` (from the parsed reviews)
   * **Operator**: `<` (less than)
   * **Value**: `2`

This routes only 1–2 star reviews to the next step.
{% endstep %}

{% step %}

### Choose Your Action

Now route the reviews to your destination. Here are common workflows:

{% tabs %}
{% tab title="Send to Slack" %}

1. Click **+** below the filter
2. Search for **Slack**
3. Select **Slack > Send a message**
4. Connect your Slack workspace
5. Choose the channel (e.g., `#app-reviews`)
6. In the **Message** field, format your review. Map fields from the HTTP response:
   * `[rating]` stars from `[author]`
   * Title: `[title]`
   * Review: `[content]`
   * Country: `[country]`
7. Click **OK**
   {% endtab %}

{% tab title="Add Row to Google Sheets" %}

1. Click **+** below the filter
2. Search for **Google Sheets**
3. Select **Google Sheets > Add a row**
4. Connect your Google account
5. Choose your spreadsheet and worksheet
6. Map the columns:
   * **Column A**: Today's date (use `now` function)
   * **Column B**: Rating (map to `rating` field)
   * **Column C**: Title (map to `title` field)
   * **Column D**: Review (map to `content` field)
   * **Column E**: Author (map to `author` field)
   * **Column F**: Country (map to `country` field)
7. Click **OK**
   {% endtab %}

{% tab title="Send Email" %}

1. Click **+** below the filter
2. Search for **Email**
3. Select **Email > Send an email**
4. Set:
   * **To**: your email or team distribution list
   * **Subject**: New critical review from \[author]
   * **Body**: Compose a formatted message with review details
5. Click **OK**
   {% endtab %}
   {% endtabs %}
   {% endstep %}
   {% endstepper %}

## Example Scenario: Daily Reviews to Google Sheets + Slack

Here's a complete, ready-to-use scenario:

1. **Module 1: Schedule** → Runs daily at 9 AM
2. **Module 2: HTTP Request** → Fetches all reviews from Pullbay
3. **Module 3: JSON Parser** → Parses the response
4. **Module 4: Router** → Filters for rating < 2
5. **Module 5: Google Sheets** → Logs all low-rated reviews to a spreadsheet
6. **Module 6: Slack** → Posts a summary to `#app-reviews`

When this scenario runs:

* It wakes up at 9 AM
* Pulls overnight reviews from Pullbay
* Filters for 1–2 star reviews
* Adds them to your review tracking sheet
* Alerts your team on Slack with the critical reviews

All hands-off, no manual copying.

## Example Scenario: Weekly Digest with Aggregation

For a higher-level view:

1. **Schedule** → Runs weekly on Monday at 8 AM
2. **HTTP Request** → Fetches all reviews
3. **Filter** → Separate into rating buckets (5-star, 4-star, 1-2 star)
4. **Aggregator** (optional Make module) → Groups reviews by rating
5. **Email** → Sends a formatted digest showing review counts and top comments by rating

## Securing Your API Key

Never hardcode your Pullbay API key in a scenario. Instead:

1. Click the **key icon** next to the Authorization header field
2. Select **Add a variable** or **Create a custom variable**
3. Name it `pullbay_api_key` and paste your API key
4. Click **OK**

Make encrypts the variable and only uses it at runtime. You can reuse this variable in all your scenarios.

## Understanding the Pullbay API Response

The HTTP module returns a JSON response like this:

```json
{
  "success": true,
  "data": [
    {
      "id": "review_123",
      "app_id": "com.example.app",
      "rating": 5,
      "title": "Great app!",
      "content": "This app changed my workflow...",
      "author": "Jane Doe",
      "author_id": "user_456",
      "version": "2.1.0",
      "date": "2026-04-12T14:30:00Z"
    },
    ...
  ]
}
```

In Make, you can map any of these fields (`rating`, `title`, `content`, `author`, `country`, etc.) to your actions.

## Rate Limits and Pricing

Your Make scenarios count against your Pullbay API plan:

* **Free**: 10 requests/minute
* **Starter**: 60 requests/minute
* **Growth**: 300 requests/minute
* **Scale**: 1000+ requests/minute

Check the [Pullbay dashboard](https://dashboard.pullbay.com/) for current pricing.

## Troubleshooting

**HTTP module returns "401 Unauthorized"**

* Verify your API key is correct and active
* Ensure the Authorization header is exactly: `Bearer YOUR_API_KEY` (with the space)
* Check that your API key hasn't expired

**No reviews returned**

* Confirm your app ID is correct
* Verify that your app actually has reviews on the App Store
* Check the response body—look for error details

**Scenario runs but takes too long**

* If you're fetching for a large app with thousands of reviews, the HTTP request may slow down
* Consider filtering by date in a custom module or post-processing the response

**Can't map fields to actions**

* Ensure you've enabled "Parse response" in the HTTP module
* Run a test execution first to populate the response data
* Click on the field in the action module and select from the dropdown

## Advanced: Custom Aggregation and Reporting

For sophisticated reporting, use Make's built-in modules:

* **Aggregator**: Group reviews by rating or country
* **Text aggregator**: Concatenate review bodies for a summary
* **Math**: Calculate average rating across all reviews
* **Array functions**: Filter or transform review data before sending

These let you build insights beyond simple logging.

## Native Integration Coming Soon

A native Pullbay module for Make is in development. When available, you'll get:

* Pre-configured fields with no manual mapping needed
* Built-in error handling and retry logic
* Faster setup and cleaner scenarios
* Direct access to Pullbay-specific features

Until then, the HTTP module approach is fully supported and powerful.

## Next Steps

1. Create a new scenario in Make
2. Set up a Schedule trigger for daily at a time that works for your team
3. Add the HTTP module and fill in your API key, app ID, and endpoint
4. Add one action (Slack, Google Sheets, or email) to start
5. Run a test execution to verify everything works
6. Enable the scenario and monitor the first run

For more on Pullbay's API, see the [API documentation](https://docs.pullbay.com/). For Make-specific help, visit [Make's help center](https://www.make.com/help).


# Zapier Integration

Zapier is a powerful automation platform that connects thousands of apps without custom code. This guide shows you how to build automated workflows with Pullbay's App Store Reviews API—today with Webhooks by Zapier, and with a native Pullbay app coming soon.

## What You Can Build with Pullbay and Zapier

The combination of Pullbay and Zapier lets you:

* **Trigger workflows on new reviews**: Set up a daily or hourly check for fresh app store reviews and automatically route them where you need them
* **Send reviews to Slack**: Post 1–2 star reviews instantly to a dedicated channel for team response
* **Log reviews to Google Sheets**: Build a live spreadsheet of all reviews with ratings, authors, and timestamps
* **Feed reviews to HubSpot**: Sync customer feedback directly into your CRM for sales and support teams
* **Alert Airtable**: Create a structured database of reviews for analysis and team collaboration
* **Email summaries**: Get daily digests of high-priority reviews in your inbox

## How Pullbay Works with Zapier

Currently, there is no native Pullbay app in Zapier's app marketplace. However, you can use **Webhooks by Zapier** and **Code by Zapier** to connect to the Pullbay API right now. A native Pullbay integration is coming soon—when it arrives, your setup will be even simpler.

{% stepper %}
{% step %}

### Create a New Zap

1. Log in to your Zapier account at [zapier.com](https://zapier.com/)
2. Click **Create** (top left)
3. You'll see the Zap editor with a **Trigger** step ready to configure
   {% endstep %}

{% step %}

### Set Up Your Trigger

For daily or hourly review checks, use **Schedule by Zapier**:

1. In the Trigger section, search for and select **Schedule by Zapier**
2. Choose **Schedule** as the trigger event
3. Set your frequency:
   * **Daily**: Runs once per day at a time you choose
   * **Weekly**: Runs on a specific day and time
   * **Monthly**: Runs on a specific date
4. Click **Continue**

(If you prefer to trigger based on webhooks or external events, choose **Webhooks by Zapier** instead and select **Catch Hook**.)
{% endstep %}

{% step %}

### Add a Code Step to Fetch Reviews

1. Click the **+** icon below your trigger to add an action
2. Search for and select **Code by Zapier**
3. Choose **Code by Zapier** (JavaScript)
4. In the input fields, add:
   * **Input Data** (optional): Create fields for:
     * `apiKey`: Your Pullbay API key
     * `appId`: Your app's ID on the App Store
5. In the code editor, paste this JavaScript code:

```javascript
const apiKey = inputData.apiKey;
const appId = inputData.appId;

const url = `https://api.pullbay.com/app-store/reviews/all?app_id=${appId}`;

const options = {
  headers: {
    'Authorization': `Bearer ${apiKey}`
  }
};

try {
  const response = await fetch(url, options);
  const data = await response.json();

  if (data.success) {
    output = {
      reviews: data.data,
      count: data.data.length,
      success: true
    };
  } else {
    output = {
      error: data.error,
      success: false
    };
  }
} catch (error) {
  output = {
    error: error.message,
    success: false
  };
}
```

6. Click **Continue**. Zapier will execute the code and show you the output structure.
   {% endstep %}

{% step %}

### Filter Reviews (Optional)

To focus on high-priority reviews, add a **Filter** step:

1. Click **+** below the Code step
2. Search for **Filter by Zapier**
3. Set up a condition like:
   * **Reviews Rating** < **2** (to catch 1–2 star reviews)
4. Click **Continue**

This ensures only critical reviews trigger your next action.
{% endstep %}

{% step %}

### Choose Your Action

Now route the reviews to your desired destination. Here are common examples:

#### Send to Slack

1. Click **+** to add an action
2. Search for **Slack**
3. Select **Send Channel Message**
4. Connect your Slack workspace and choose the channel (e.g., `#app-reviews`)
5. In the message body, format your review:

```
New App Review:
Rating: [Rating] ⭐
Author: [Author]
Title: [Title]
Review: [Review Text]
Country: [Country]
```

6. Click **Continue** and test

#### Log to Google Sheets

1. Click **+** to add an action
2. Search for **Google Sheets**
3. Select **Create Spreadsheet Row**
4. Connect your Google account and choose your spreadsheet and worksheet
5. Map the Pullbay review fields to columns:
   * Column A (Date): Today's date
   * Column B (Rating): `[Rating]`
   * Column C (Title): `[Title]`
   * Column D (Review): `[Review]`
   * Column E (Author): `[Author]`
   * Column F (Country): `[Country]`
6. Click **Continue** and test

#### Send to HubSpot

1. Click **+** to add an action
2. Search for **HubSpot**
3. Select **Create a Note** (or **Create Task** if you prefer)
4. Map review data to HubSpot fields
5. Test the connection
   {% endstep %}
   {% endstepper %}

## Example Complete Zap: Daily Review Check → Slack Alert

Here's a real-world workflow:

1. **Trigger**: Schedule by Zapier (Daily, 9 AM)
2. **Action 1**: Code by Zapier (fetch reviews from Pullbay using the JavaScript above)
3. **Action 2**: Filter by Zapier (Rating < 2)
4. **Action 3**: Slack (Send message to `#app-reviews` with formatted review)

This Zap runs every morning, pulls overnight reviews, filters for 1–2 star ratings, and posts them to your team Slack channel—no manual effort required.

## Storing Your API Key Securely

Never hardcode your Pullbay API key in a Zap. Instead:

1. Store your API key as a **Zapier Secret**:
   * In your Zap, click the **key icon** next to an input field
   * Select **Create a Secret**
   * Paste your API key and name it (e.g., `pullbay_api_key`)
2. In your Code step, reference it: `const apiKey = process.env.pullbay_api_key;`

Zapier encrypts secrets and only passes them to code at runtime.

## Troubleshooting

<details>

<summary>Code step returns "error: Unauthorized"</summary>

* Verify your API key is correct and active in the Pullbay dashboard
* Ensure your API plan supports the request rate (Free = 10 req/min, Starter = 60 req/min, etc.)

</details>

<details>

<summary>No reviews returned</summary>

* Check that your `appId` is correct and matches your actual app ID on the App Store
* Verify that your app has reviews (new apps may have none)

</details>

<details>

<summary>Slack message formatting looks broken</summary>

* Use plain text in the message format—Slack's markdown will render bold, links, and line breaks
* Test with a single review first

</details>

## API Rate Limits and Pricing

Zapier Zaps run on your Pullbay API plan:

* **Free**: 10 requests/minute
* **Starter**: 60 requests/minute
* **Growth**: 300 requests/minute
* **Scale**: 1000+ requests/minute

See the [Pullbay dashboard](https://dashboard.pullbay.com/) for current pricing and plan details.

## Native Integration Coming Soon

A native Pullbay app for Zapier is in development. When available, you'll be able to:

* Skip the Code step entirely
* Use a pre-built Pullbay trigger
* Access Pullbay fields in a simple, visual interface
* Benefit from built-in error handling and retries

Until then, the Webhooks by Zapier + Code approach works perfectly and gives you full flexibility.

## Next Steps

* Set up your first Zap using the steps above
* Test with a schedule trigger and filter
* Connect to your preferred action (Slack, Sheets, HubSpot, etc.)
* Monitor the Zap for the first day to ensure reviews are flowing correctly

For questions about the Pullbay API itself, see the [API documentation](https://docs.pullbay.com/). For Zapier-specific issues, check [Zapier's help center](https://zapier.com/help).


# Pullbay X Google Sheets Integration

Google Sheets is the perfect home for App Store review data. It's free, shareable, and lets you analyze reviews with charts, pivot tables, and filters — no coding skills required. This guide shows you how to automatically sync Pullbay reviews to a Google Sheet using Google Apps Script.

## Why Log Reviews to Google Sheets?

* **Shareable**: Share the sheet with your team, stakeholders, or investors in seconds
* **Searchable**: Filter reviews by rating, date, country, or keywords
* **Visual**: Create charts showing review trends over time
* **Collaborative**: Add comments, assign follow-ups, and tag team members
* **No-code analysis**: Use built-in functions (AVERAGE, COUNTIF, etc.) to calculate metrics
* **Backup**: Keep a permanent record of all reviews with timestamps

## Two Approaches

1. **Google Apps Script** (recommended) — write a simple script that calls the Pullbay API and logs reviews directly
2. **Via automation platforms** — use Zapier or Make.com as middleware if you prefer not to write code

This guide focuses on the Google Apps Script approach, which is free and requires minimal setup.

## Approach 1: Google Apps Script (Direct API Call)

### Prerequisites

* A Google account and access to Google Sheets
* Your Pullbay API key (from the [dashboard](https://dashboard.pullbay.com/))
* Your app's numeric App Store ID (8–12 digits) or bundle ID (e.g. `com.burbn.instagram`)

{% stepper %}
{% step %}

## Create a Google Sheet

1. Go to [sheets.google.com](https://sheets.google.com/)
2. Click **Create** and select **Blank spreadsheet**
3. Name it (e.g. "App Store Reviews - 2026")
4. Click **OK**
   {% endstep %}

{% step %}

## Set Up Sheet Columns

In row 1, create these column headers:

| Column | Header    | Source field         |
| ------ | --------- | -------------------- |
| A      | Date      | `review.date`        |
| B      | Score     | `review.score` (1–5) |
| C      | Title     | `review.title`       |
| D      | Review    | `review.text`        |
| E      | Author    | `review.userName`    |
| F      | Version   | `review.version`     |
| G      | Country   | `review.country`     |
| H      | Review ID | `review.id`          |

Starting in row 2, the script will fill in review data automatically.
{% endstep %}

{% step %}

## Open the Apps Script Editor

1. In your Google Sheet, click **Extensions** (top menu)
2. Click **Apps Script**
3. A new tab opens with the Apps Script editor
4. Delete the default `myFunction()` that appears
   {% endstep %}

{% step %}

## Write the Review-Fetching Script

Copy and paste this complete script into the editor:

```javascript
function fetchAndLogReviews() {
  // ── Configuration ────────────────────────────────────────────────────────
  const API_KEY = "YOUR_API_KEY";  // Replace with your Pullbay API key
  const APP_ID  = "389801252";     // Numeric App Store ID (or use BUNDLE_ID below)
  // const BUNDLE_ID = "com.burbn.instagram"; // Alternative: use bundle ID instead of APP_ID
  const MAX_ITEMS = 500;           // Maximum reviews per run (1–500)

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();

  // ── Build request URL ────────────────────────────────────────────────────
  const params = new URLSearchParams({
    appId:    APP_ID,      // Use bundleId: BUNDLE_ID if using bundle ID
    country:  "us",
    sort:     "recent",
    maxItems: MAX_ITEMS,
  });

  const url = `https://api.pullbay.com/appstore/reviews?${params}`;

  const options = {
    method:            "get",
    headers:           { "Authorization": `Bearer ${API_KEY}` },
    muteHttpExceptions: true,
  };

  // ── Fetch reviews ─────────────────────────────────────────────────────────
  try {
    const response     = UrlFetchApp.fetch(url, options);
    const responseCode = response.getResponseCode();

    if (responseCode !== 200) {
      Logger.log(`Error: API returned ${responseCode}`);
      Logger.log(response.getContentText());
      return;
    }

    const body = JSON.parse(response.getContentText());

    if (!body.success || !body.data) {
      Logger.log("Error: unexpected response from Pullbay");
      Logger.log(JSON.stringify(body));
      return;
    }

    const reviews       = body.data;
    const creditsCharged = body.pricing.creditsCharged;
    Logger.log(`Fetched ${reviews.length} reviews — requestId: ${body.requestId}, credits: ${creditsCharged}`);

    // ── Clear existing data (comment out to append instead) ───────────────
    const lastRow = sheet.getLastRow();
    if (lastRow > 1) {
      sheet.deleteRows(2, lastRow - 1);
    }

    // ── Write reviews to sheet ─────────────────────────────────────────────
    reviews.forEach((review, index) => {
      const row  = index + 2; // Row 1 is headers
      const date = new Date(review.date).toLocaleDateString();

      sheet.getRange(row, 1).setValue(date);             // Date
      sheet.getRange(row, 2).setValue(review.score);     // Score (1–5)
      sheet.getRange(row, 3).setValue(review.title);     // Title
      sheet.getRange(row, 4).setValue(review.text);      // Review text
      sheet.getRange(row, 5).setValue(review.userName);  // Author
      sheet.getRange(row, 6).setValue(review.version);   // App version
      sheet.getRange(row, 7).setValue(review.country);   // Country
      sheet.getRange(row, 8).setValue(review.id);        // Review ID
    });

    Logger.log(`Successfully logged ${reviews.length} reviews`);

  } catch (error) {
    Logger.log(`Error: ${error.toString()}`);
  }
}
```

{% endstep %}

{% step %}

## Add Your API Key and App ID

In the script, replace:

* `YOUR_API_KEY` with your actual Pullbay API key
* `389801252` with your App Store app ID

If you prefer to use the bundle ID (e.g. `com.burbn.instagram`), comment out the `appId` line and uncomment `bundleId`.
{% endstep %}

{% step %}

## Test the Script

1. In the Apps Script editor, click the **Run** button (play icon)
2. If prompted for permissions, click **Review permissions**, select your Google account, and click **Allow**
3. Check the **Execution log** (bottom panel) for output messages

Go back to your Google Sheet. If successful, you'll see review data from row 2 onward.
{% endstep %}

{% step %}

## Set Up Automatic Daily Runs

To run the script automatically every day:

1. In the Apps Script editor, click the **Triggers** icon (clock) on the left sidebar
2. Click **Create a trigger**
3. Configure the trigger:
   * **Function to run**: `fetchAndLogReviews`
   * **Deployment**: Head
   * **Event source**: Time-driven
   * **Type**: Day timer
   * **Time of day**: e.g. 9 AM
4. Click **Save**

The script now runs automatically at your chosen time every day. Check the **Executions** log in Apps Script to verify runs succeeded.
{% endstep %}
{% endstepper %}

## Understanding the Script

The script does four things:

1. **Builds the request URL** with `appId`, `country`, `sort`, and `maxItems` as query parameters
2. **Calls the Pullbay API** with your Bearer token and parses the response envelope (`body.data`, `body.pricing.creditsCharged`, `body.requestId`)
3. **Clears previous rows** starting from row 2 (optional — comment out to append instead)
4. **Writes each review** to a new row using the actual field names from the API: `score`, `text`, `userName`, `version`, `country`, `id`

To **append** new reviews on every run instead of replacing, comment out the delete block:

```javascript
// const lastRow = sheet.getLastRow();
// if (lastRow > 1) {
//   sheet.deleteRows(2, lastRow - 1);
// }
```

And change the row assignment in the forEach loop:

```javascript
reviews.forEach((review) => {
  const row = sheet.getLastRow() + 1; // Append to next empty row
  // ... rest unchanged
});
```

## Approach 2: Using Zapier or Make as Middleware

If you prefer not to write scripts, you can use Zapier or Make.com:

1. Follow the Zapier or Make.com integration guides
2. In the final action, select **Add row to Google Sheets**
3. Map the Pullbay fields to your sheet columns using the field names above (`score`, `text`, `userName`, etc.)
4. Reviews are logged automatically on your chosen schedule

## Tips for Analysis and Organization

### Color-Code Reviews by Rating

1. Select the **Score** column (column B)
2. Click **Format** → **Conditional formatting**
3. Add rules:
   * Score = 5: Green background
   * Score = 4: Light green background
   * Score ≤ 2: Red background
4. Click **Done**

High-priority reviews stand out at a glance.

### Create a Pivot Table for Review Trends

1. Select all your data (including headers)
2. Click **Insert** → **Pivot table** → **Create**
3. In the pivot table editor:
   * **Rows**: Country
   * **Columns**: Score
   * **Values**: COUNTA of Review ID
4. Click **Insert**

Example output:

| Country | 1 | 2 | 3  | 4  | 5  |
| ------- | - | - | -- | -- | -- |
| US      | 3 | 5 | 12 | 24 | 45 |
| GB      | 1 | 2 | 8  | 18 | 32 |

### Calculate Key Metrics

Add formulas in a dedicated "Metrics" sheet or row:

* **Average score**: `=AVERAGE(B2:B)`
* **Total reviews**: `=COUNTA(H2:H)` (counts non-empty Review ID cells)
* **Low-rating count**: `=COUNTIF(B2:B,"<=2")`
* **5-star count**: `=COUNTIF(B2:B,5)`
* **Latest review date**: `=MAX(A2:A)`

### Set Up Filters

1. Click any cell in your data
2. Click **Data** → **Create a filter**
3. Use the filter icons on the header row to show only 1–2 star reviews, a specific country, or a date range

Filters let you focus without changing the underlying data.

### Use Multiple Sheets Per App

If you manage multiple apps:

1. Create a separate sheet tab for each app (rename tabs at the bottom)
2. Modify the script to write to a named sheet instead of the active one:

```javascript
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Instagram");
```

3. Create one trigger per app or pass the app ID as a parameter

## Modifying the Script for Advanced Use

### Filter by Date (Last 7 Days Only)

```javascript
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);

const recentReviews = reviews.filter(review => new Date(review.date) >= sevenDaysAgo);
// Use recentReviews in the forEach loop below
```

### Send a Summary Email When Complete

Add this at the end of the script to receive an email after each run:

```javascript
GmailApp.sendEmail(
  Session.getEffectiveUser().getEmail(),
  "Reviews Synced",
  `${reviews.length} reviews logged — ${creditsCharged} credits used — ${new Date().toLocaleString()}`
);
```

### Paginate Manually (for Very Large Datasets)

`maxItems=500` fetches up to 500 reviews in one call (the endpoint maximum). If you want to paginate manually across multiple runs using cursors, store the cursor value in a script property:

```javascript
const props  = PropertiesService.getScriptProperties();
const cursor = props.getProperty("reviewCursor");

const params = new URLSearchParams({ appId: APP_ID, country: "us", sort: "recent" });
if (cursor) params.set("cursor", cursor);

// After fetching...
const pagination = body.pagination;
if (pagination.hasNextPage && pagination.cursor) {
  props.setProperty("reviewCursor", pagination.cursor);
} else {
  props.deleteProperty("reviewCursor"); // No more pages; reset
}
```

## Troubleshooting

<details>

<summary>Script runs but no data appears in the sheet</summary>

* Check the Execution log (**View → Execution log**) for error messages
* Verify your API key is correct and active in the Pullbay dashboard
* Confirm your app ID is a valid 8–12 digit number (or that the bundle ID matches exactly)
* Check that your app has reviews in the selected country

</details>

<details>

<summary>Authentication error / 401 response</summary>

* Your API key may have been revoked or expired
* Generate a new key in the [Pullbay dashboard](https://dashboard.pullbay.com/) and update the script

</details>

<details>

<summary>Script times out</summary>

* Apps Script has a 6-minute execution limit
* Reduce `MAX_ITEMS` (e.g. to 100) or add a date filter so fewer rows are written per run
* For very large review sets, paginate across multiple scheduled runs using script properties (see above)

</details>

<details>

<summary>Need to change the API key or app ID later</summary>

* Click **Extensions** → **Apps Script**
* Update the values at the top of the script
* Save and re-run to verify

</details>

## Rate Limits and Credits

Each time the script runs, it makes one API request. Credits consumed depend on the number of reviews returned:

```
creditsCharged = 1 (base) + returned_reviews × 1
```

For example: 200 reviews returned → 201 credits charged.

Rate limits are enforced per API key based on your plan (per-second and per-minute). Running once per day is well within all plan limits. Check the `X-RateLimit-Remaining` response header or your Pullbay dashboard to monitor usage.

## Next Steps

1. Create your Google Sheet and add the column headers from Step 2
2. Copy the Apps Script code and set your API key and app ID
3. Test the script with the **Run** button and verify the Execution log
4. Set up a time-based trigger for automatic daily runs
5. Add conditional formatting, pivot tables, and metric formulas
6. Share the sheet with your team for collaborative review management


# Pullbay x n8n Integration

n8n is a powerful open-source and cloud-based workflow automation platform that lets you build complex data pipelines without writing code. This guide shows you how to integrate Pullbay with n8n to automate App Store data collection, monitoring, and analysis workflows.

## What Is n8n?

n8n is a visual workflow automation tool that connects APIs, databases, and applications through a drag-and-drop interface. You can:

* Build complex data pipelines without coding
* Schedule recurring workflows (daily, hourly, weekly, etc.)
* Connect to 400+ apps and services (Google Sheets, Slack, Airtable, etc.)
* Transform and process data between systems
* Trigger workflows based on events or schedules

## Why Use n8n with Pullbay?

### No-Code Integration

Build workflows without any programming knowledge using the visual workflow builder.

### Seamless Integration with Business Tools

Connect Pullbay data directly to:

* **Google Sheets** — automatically populate spreadsheets with review data
* **Slack** — send alerts when low-score reviews appear
* **Airtable** — store structured review data for team collaboration
* **Webhooks** — trigger actions in your own applications
* **Databases** — write data directly to PostgreSQL, MySQL, or other databases

### Bulk Fetch with `maxItems`

Use the `maxItems` parameter to have Pullbay auto-paginate Apple's API internally and return up to 500 reviews in a single response. No pagination loops needed in your workflow.

### Scheduled Execution

Configure workflows to run on a schedule: daily collection at midnight, hourly monitoring for low-score feedback, or weekly competitive analysis reports.

### Error Handling and Reliability

n8n includes built-in error handling, retry logic, and notifications so you know when something goes wrong.

## Setup: Storing Your API Key

Before creating workflows, store your Pullbay API key as a reusable credential in n8n.

{% stepper %}
{% step %}

## Access Credentials

1. Log in to n8n
2. Click **Credentials** in the left sidebar
3. Click **Create New**
   {% endstep %}

{% step %}

## Create Header Authentication Credential

1. Select **Header Auth** from the credential type dropdown
2. Fill in the following:

| Field               | Value                 |
| ------------------- | --------------------- |
| **Credential Name** | `Pullbay API`         |
| **Header Name**     | `Authorization`       |
| **Header Value**    | `Bearer YOUR_API_KEY` |

3. Replace `YOUR_API_KEY` with your actual key from the Pullbay dashboard
4. Click **Create**

Your credential is now stored securely and reusable across all workflows.
{% endstep %}
{% endstepper %}

## HTTP Request Node Configuration

All Pullbay workflows use the **HTTP Request** node with this base configuration:

| Setting              | Value                                              |
| -------------------- | -------------------------------------------------- |
| **Method**           | GET                                                |
| **URL**              | `https://api.pullbay.com/appstore/reviews`         |
| **Authentication**   | Header Auth — select your `Pullbay API` credential |
| **Query Parameters** | See examples below                                 |

### Query Parameters

| Parameter  | Value                     | Notes                                                          |
| ---------- | ------------------------- | -------------------------------------------------------------- |
| `appId`    | Your App Store numeric ID | Required (8–12 digits). Mutually exclusive with `bundleId`.    |
| `bundleId` | Your app bundle ID        | Alternative to `appId`. E.g. `com.burbn.instagram`.            |
| `country`  | 2-letter country code     | Optional. Default: `us`                                        |
| `sort`     | `recent` or `helpful`     | Optional. Default: `recent`                                    |
| `maxItems` | Integer 1–500             | Optional. Omit to fetch one page (\~50 reviews) with a cursor. |
| `page`     | Integer 1–10              | Optional. Mutually exclusive with `maxItems`.                  |

**`maxItems` and `page` cannot be used together.**

## Fetch Mode: `maxItems` vs. `page`

Pullbay has two fetch modes on the same endpoint:

| Mode            | Parameter          | Behavior                                                                                                          | Use in n8n                               |
| --------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| **Bulk**        | `maxItems=500`     | Pullbay auto-paginates Apple's API internally and returns all results in one response. No cursor in the response. | Recommended for most workflows           |
| **Single page** | `page=1` (or omit) | Returns \~50 reviews and a pagination cursor.                                                                     | Use only if you need per-page processing |

For n8n workflows, **always use `maxItems`**. Single-page pagination requires loop logic that is complex to manage in a visual workflow builder.

## Basic Workflow Structure

Every Pullbay workflow in n8n follows this pattern:

```
[Trigger] → [HTTP Request] → [Set: extract data] → [Process/Filter] → [Destination]
```

1. **Trigger** — schedule, webhook, or manual start
2. **HTTP Request** — fetch data from Pullbay (`/api/appstore/reviews`)
3. **Set** — extract `{{ $json.data }}` from the response envelope
4. **Process/Filter** — transform, filter, or split reviews as needed
5. **Destination** — Google Sheets, Slack, Airtable, database, etc.

## Example Workflows

### Workflow 1: Daily Review Collection to Google Sheets

Automatically fetch app reviews every day at 9 AM and save them to Google Sheets.

{% stepper %}
{% step %}

## Schedule Trigger

* Trigger type: Every day
* Time: 9:00 AM
  {% endstep %}

{% step %}

## HTTP Request — fetch reviews

* Method: GET
* URL: `https://api.pullbay.com/appstore/reviews`
* Authentication: Pullbay API (Header Auth)
* Query parameters:
  * `appId`: `284882215`
  * `country`: `us`
  * `sort`: `recent`
  * `maxItems`: `500`
    {% endstep %}

{% step %}

## Set Node — extract reviews array

* Set a field `reviews` to `{{ $json.data }}`
  {% endstep %}

{% step %}

## Split In Batches

* Batch size: 1 (one review per row)
  {% endstep %}

{% step %}

{% endstep %}

{% step %}

## Google Sheets — Append Row

{% endstep %}

{% step %}

* Document: your Google Sheets document
* Sheet: `Reviews`
* Columns mapped from review fields:
  {% endstep %}

{% step %}

## Slack or Email Notification

* Notify when the run completes

**Expected outcome**: Your Google Sheets document is updated every morning with the latest reviews, creating a historical record of app feedback.
{% endstep %}
{% endstepper %}

| Sheet Column | n8n Expression         |
| ------------ | ---------------------- |
| Date         | `{{ $json.date }}`     |
| Score        | `{{ $json.score }}`    |
| Title        | `{{ $json.title }}`    |
| Review       | `{{ $json.text }}`     |
| Author       | `{{ $json.userName }}` |
| Version      | `{{ $json.version }}`  |
| Country      | `{{ $json.country }}`  |
| Review ID    | `{{ $json.id }}`       |
| {% endstep}  |                        |

### Workflow 2: Review Monitoring with Slack Alerts

Hourly monitoring that sends a Slack message for every review with a score ≤ 2.

{% stepper %}
{% step %}

## Schedule Trigger

* Trigger type: Every hour
  {% endstep %}

{% step %}

## HTTP Request — fetch recent reviews

* URL: `https://api.pullbay.com/appstore/reviews`
* Query parameters:
  * `appId`: `284882215`
  * `country`: `us`
  * `sort`: `recent`
  * `maxItems`: `100`
    {% endstep %}

{% step %}

## Set Node

* Set `reviews` to `{{ $json.data }}`
  {% endstep %}

{% step %}

## Split In Batches

* Batch size: 1
  {% endstep %}

{% step %}

## If Node — filter low scores

* Condition: `{{ $json.score }}` ≤ `2`
* True branch: continue to Slack
* False branch: No Operation
  {% endstep %}

{% step %}

## Slack — Send Message

```
🚨 Critical Review Alert
Author: {{ $json.userName }}
Score: {{ $json.score }} / 5
Title: {{ $json.title }}
Review: {{ $json.text }}
Version: {{ $json.version }}
Country: {{ $json.country }}
Date: {{ $json.date }}
```

**Expected outcome**: Your team gets an instant Slack notification for every 1–2 star review, allowing fast responses to critical feedback.
{% endstep %}
{% endstepper %}

### Workflow 3: Competitive Analysis (Multiple Apps, Weekly)

Monitor competitor apps and compile findings into a weekly report.

{% stepper %}
{% step %}

## Schedule Trigger

* Every Monday at 9:00 AM
  {% endstep %}

{% step %}

## Code Node — define apps to monitor

```javascript
return [
  { json: { appId: "284882215", name: "Facebook" } },
  { json: { appId: "389801252", name: "Instagram" } },
  { json: { appId: "310633997", name: "WhatsApp" } },
];
```

{% endstep %}

{% step %}

## HTTP Request — fetch reviews for each app

* URL: `https://api.pullbay.com/appstore/reviews`
* Query parameters:
  * `appId`: `{{ $json.appId }}`
  * `country`: `us`
  * `sort`: `recent`
  * `maxItems`: `100`
    {% endstep %}

{% step %}

## Code Node — build summary

```javascript
const appName = $('Code Node').first().json.name;
const reviews = $json.data;
const scores  = reviews.map(r => r.score);
const avg     = scores.reduce((a, b) => a + b, 0) / scores.length;

return [{
  json: {
    app:           appName,
    totalReviews:  reviews.length,
    averageScore:  avg.toFixed(2),
    lowScoreCount: scores.filter(s => s <= 2).length,
    date:          new Date().toISOString(),
  }
}];
```

{% endstep %}

{% step %}

## Google Sheets — Append Row

* Write summary: date, app name, average score, review count, low-score count
  {% endstep %}

{% step %}

## Slack Notification

* Send weekly summary to your product team

**Expected outcome**: Every Monday, your team receives a competitive intelligence report showing how competitor apps are being reviewed.
{% endstep %}
{% endstepper %}

### Workflow 4: Sentiment Analysis with OpenAI

Fetch reviews and automatically analyze sentiment using OpenAI.

{% stepper %}
{% step %}

## Schedule Trigger

* Every day at midnight
  {% endstep %}

{% step %}

## HTTP Request — fetch recent reviews

* URL: `https://api.pullbay.com/appstore/reviews`
* Query parameters:
  * `appId`: `284882215`
  * `sort`: `recent`
  * `maxItems`: `50`
    {% endstep %}

{% step %}

## Set Node

* Set `reviews` to `{{ $json.data }}`
  {% endstep %}

{% step %}

## Split In Batches

* Batch size: 1
  {% endstep %}

{% step %}

## OpenAI Node — analyze sentiment

```
Analyze the sentiment of this app review. Respond with:
- sentiment: positive, negative, or neutral
- score: -1 to 1
- topics: comma-separated key topics

Review title: {{ $json.title }}
Review text: {{ $json.text }}
Star score: {{ $json.score }}/5
```

{% endstep %}

{% step %}

## Airtable — Store Results

* Review text: `{{ $json.text }}`
* Star score: `{{ $json.score }}`
* AI sentiment: from OpenAI response
* AI score: from OpenAI response
* Topics: from OpenAI response
* Analyzed at: `{{ new Date().toISOString() }}`
  {% endstep %}

{% step %}

## Notification

* Alert when negative sentiment exceeds a threshold

**Expected outcome**: A database of reviews with AI-powered sentiment analysis, identifying trends and emotional patterns in customer feedback.
{% endstep %}
{% endstepper %}

## Handling Response Data in n8n

The Pullbay API always returns the same envelope. In the Set node, extract the reviews array:

```
{{ $json.data }}
```

Log credit usage and request ID after each call:

```
Credits charged: {{ $json.pricing.creditsCharged }}
Request ID:      {{ $json.requestId }}
```

### Individual Review Field Reference

After **Split In Batches** (batch size 1), each item is a single review. Reference fields as:

| Field       | n8n Expression         | Description                 |
| ----------- | ---------------------- | --------------------------- |
| Review ID   | `{{ $json.id }}`       | Unique review identifier    |
| Date        | `{{ $json.date }}`     | ISO 8601 datetime           |
| Score       | `{{ $json.score }}`    | Integer 1–5                 |
| Title       | `{{ $json.title }}`    | Review headline             |
| Review text | `{{ $json.text }}`     | Full review body            |
| Author      | `{{ $json.userName }}` | Reviewer display name       |
| App version | `{{ $json.version }}`  | App version reviewed        |
| Country     | `{{ $json.country }}`  | 2-letter country code       |
| Review URL  | `{{ $json.url }}`      | Link to the review on Apple |

## Error Handling

### Configure Timeout

In the HTTP Request node, click **Additional options** and set **Timeout** to `120000` ms (2 minutes). This gives the bulk fetch endpoint enough time to retrieve all pages internally when `maxItems` is large.

### Enable Retry Logic

{% stepper %}
{% step %}

## Enable Retry Logic

1. In the HTTP Request node settings, enable **Retry On Fail**
2. Set retry attempts to **3**
3. Enable **Continue On Fail** if you want the workflow to keep running even when one request fails
   {% endstep %}

{% step %}

## Add an Error Path

1. Connect an error handler path from the HTTP Request node
2. Send a Slack or email notification when the API call fails

Example Slack error message:

```
⚠️ Pullbay API Error
Status:    {{ $json.statusCode }}
Message:   {{ $json.message }}
requestId: {{ $json.requestId }}
Time:      {{ new Date().toISOString() }}
```

{% endstep %}
{% endstepper %}

## Tips and Best Practices

### Test with Small `maxItems` First

When building a workflow, test with a small value to iterate quickly:

```
maxItems=10
```

Once the workflow is working correctly, increase `maxItems` up to 500.

### Track Credit Usage

Log `{{ $json.pricing.creditsCharged }}` after each HTTP Request node so you can monitor credit consumption over time. You can write this value to a dedicated "Credits" sheet in Google Sheets.

### Deduplicate Reviews

If running workflows frequently, track processed review IDs to avoid duplicates:

1. Store `{{ $json.id }}` values in a database or Google Sheet after each run
2. In the If node, skip reviews whose ID already exists in your log
3. Alternatively, filter by `{{ $json.date }}` to process only reviews newer than your last run

### Split Large Datasets

After the Set node, add a **Split In Batches** node before any per-review processing:

* Batch size **1** for per-review actions (Slack message, OpenAI call, etc.)
* Batch size **50–100** for bulk writes (Google Sheets append rows)

Splitting prevents memory issues and enables per-batch error handling.

### Schedule During Off-Peak Hours

For large `maxItems` values, schedule workflows during off-peak hours (typically 10 PM – 6 AM local time) for best performance.

## FAQ

<details>

<summary>Why should I use `maxItems` instead of `page` in n8n?</summary>

With `maxItems`, Pullbay paginates Apple's API internally and returns all results in a single HTTP response. With `page`, you get one page (\~50 reviews) and must build a loop with cursor tracking — complex in a no-code workflow builder. Use `maxItems` for simplicity; use `page` only if you need per-page processing control.

</details>

<details>

<summary>How do I set the timeout for bulk requests?</summary>

In the HTTP Request node, click **Additional options** and set **Timeout** to `120000` ms. This gives the endpoint 2 minutes to fetch all pages internally before returning.

</details>

<details>

<summary>Can I process individual reviews in n8n?</summary>

Yes. After the Set node, add a **Split In Batches** node with batch size 1. Each execution of the next node then receives a single review object.

</details>

<details>

<summary>What if a workflow fails?</summary>

Check the execution history in n8n. Common causes:

* Invalid `appId` — confirm the numeric ID is correct (8–12 digits)
* `429 Too Many Requests` — add retry logic with backoff
* Timeout — increase the timeout or reduce `maxItems`
* `401 Unauthorized` — verify the API key in your credential

</details>

<details>

<summary>Can I test my workflow without consuming credits?</summary>

Every real API call consumes credits. To minimise cost during development, test with `maxItems=5` to fetch only 5 reviews and confirm the workflow structure is correct before scaling up.

</details>

<details>

<summary>Can I run the same workflow for multiple apps?</summary>

Yes. Use a **Code** node to emit one item per app ID, then connect it to the HTTP Request node. n8n runs the HTTP Request for each item, so each app is fetched in sequence. See Workflow 3 above for the pattern.

</details>

## Troubleshooting

**`401 Unauthorized`**

Your API key is invalid or missing from the credential. Re-check the Header Auth credential: Header Name must be `Authorization`, Header Value must be `Bearer <your_key>` (with the word "Bearer" and a space before the key).

**`400 Bad Request`**

A query parameter is invalid. Common mistakes:

* Passing both `maxItems` and `page` in the same request — use only one
* Passing both `appId` and `bundleId` — use only one
* Invalid `sort` value — only `recent` and `helpful` are accepted
* `appId` contains non-numeric characters or is outside 8–12 digits

**`402 Insufficient Credits`**

Your account has run out of credits. Add credits from the Pullbay dashboard before re-running the workflow.

**`404 Not Found`**

Wrong URL. The correct endpoint is exactly:

```
https://api.pullbay.com/appstore/reviews
```

**Workflow runs but no data appears in the destination**

* Verify the Set node expression is `{{ $json.data }}` (not `$json.reviews` or similar)
* Check that the app has reviews in the requested country
* Look at the HTTP Request output panel in the n8n execution view to confirm what the API returned

**Workflow times out**

* Increase the HTTP Request timeout to `120000` ms
* Reduce `maxItems` (e.g. `100` instead of `500`) so there are fewer internal pages to fetch
* Ensure you are not running several large workflows concurrently on the same API key

## Next Steps

1. Store your Pullbay API key as a Header Auth credential in n8n
2. Build and test a simple HTTP Request node with `appId` and `maxItems=10`
3. Add a Set node to extract `{{ $json.data }}` and a Split In Batches node (batch size 1)
4. Connect to your destination (Google Sheets, Slack, Airtable)
5. Set a time-based trigger and monitor the first scheduled run
6. Scale up `maxItems` once the workflow is validated end-to-end

For n8n support, visit [docs.n8n.io](https://docs.n8n.io/). For Pullbay-specific questions, contact support through your dashboard.


# Api Key Security

Your API key is the primary credential for accessing Pullbay's data API. Protecting it is essential to prevent unauthorized access to your account, protect your credit balance, and ensure the integrity of your data. This guide covers best practices, emergency procedures, and infrastructure recommendations for securing your Pullbay API keys.

## The Importance of API Key Security

Your Pullbay API key grants full access to your account and all associated data. If compromised, an attacker could:

* Deplete your credit balance by making unnecessary API requests
* Access all data your keys are authorized to retrieve
* Impersonate your application or service
* Potentially violate data regulations if the attacker misuses the data
* Damage your reputation and that of your organization

Proper API key management is not optional—it's a fundamental part of operating a secure data pipeline.

## API Key Security Fundamentals

### Do's — Security Best Practices

| Practice                                                           | Benefit                                                  |
| ------------------------------------------------------------------ | -------------------------------------------------------- |
| Store keys in environment variables or secret managers             | Separates credentials from code                          |
| Use different keys for different environments (prod, staging, dev) | Limits blast radius if one key is compromised            |
| Use different keys for different applications or services          | Isolates access and simplifies revocation                |
| Use descriptive, systematic key names                              | Makes it easy to identify which key serves which purpose |
| Rotate keys periodically (every 90 days)                           | Reduces the window of exposure if an old key is leaked   |
| Revoke keys immediately if exposed                                 | Prevents continued misuse                                |
| Use the minimum number of keys you need                            | Reduces attack surface and management overhead           |
| Monitor key activity regularly                                     | Detects suspicious usage patterns early                  |
| Implement least-privilege access principles                        | Only grant keys the minimum permissions required         |
| Document your key management process                               | Ensures consistency across your team                     |

### Don'ts — Dangerous Practices to Avoid

| Anti-Pattern                                                   | Why It's Dangerous                                               |
| -------------------------------------------------------------- | ---------------------------------------------------------------- |
| Commit API keys to version control (Git, GitHub, etc.)         | Once in Git history, the key is exposed forever                  |
| Include API keys in client-side code (JavaScript, mobile apps) | Anyone can inspect your app and steal the key                    |
| Share API keys via email, Slack, or chat applications          | These channels are not secure and create audit trail problems    |
| Use the same API key across all projects and environments      | A compromise in one project exposes all of them                  |
| Keep unused API keys active                                    | Increases your attack surface and management burden              |
| Log or display API keys in error messages                      | Logs and error messages may be accessible to unauthorized people |
| Hard-code API keys in configuration files                      | Makes key rotation difficult and increases exposure risk         |
| Share keys between team members via insecure methods           | Creates a breakdown in access control and auditability           |

## If Your API Key Is Exposed

**Immediate Actions (within minutes):**

{% stepper %}
{% step %}

#### Revoke the Compromised Key Immediately

* Log into your Pullbay account
* Navigate to API Keys management
* Click "Revoke" on the exposed key
* This stops any new requests using that key
  {% endstep %}

{% step %}

#### Create a New API Key

* In the API Keys section, click "Generate New Key"
* Use a descriptive name (e.g., `prod-api-server-rotated-2026-04`)
* Note the new key securely
  {% endstep %}

{% step %}

#### Update Your Application

* Deploy the new API key to all systems using the old one
* Update environment variables, secret managers, and configuration
* Verify all services are using the new key before proceeding
  {% endstep %}

{% step %}

#### Verify the Update

* Test each application/service to confirm it's working with the new key
* Check the API dashboard to confirm requests are coming from the new key
* Monitor error rates to catch any services that are still using the old key
  {% endstep %}

{% step %}

#### Review Usage and Investigate

* Check your usage dashboard for suspicious activity during the exposure window
* Look for unusual request patterns, unexpected endpoints, or geographic anomalies
* If you find malicious activity, investigate the source
* Consider implementing additional monitoring and alerts
  {% endstep %}
  {% endstepper %}

**Follow-up Actions (within 24 hours):**

* Check your credit balance for unexpected charges
* Review recent API request logs for any unauthorized data access
* Communicate with your team about the incident
* Update your incident response and key management procedures
* Consider rotating other keys as a precaution
* If the key was exposed in a public repository, notify GitHub and other platforms so they can scan and remove it

**Long-term Actions (ongoing):**

* Implement automated secret scanning in your repositories
* Set up alerts for unusual API activity
* Establish a regular key rotation schedule
* Document the incident and lessons learned

## API Key Management Features

### Multiple API Keys

Pullbay allows you to create and manage multiple API keys within a single account. This is one of your most important security tools.

**Use Cases for Multiple Keys:**

| Key Name Example        | Purpose                     | Environment | Usage                     |
| ----------------------- | --------------------------- | ----------- | ------------------------- |
| `prod-api-server`       | Production application      | Production  | Main data pipeline        |
| `prod-data-warehouse`   | Data warehouse integration  | Production  | Scheduled ETL jobs        |
| `staging-data-pipeline` | Staging application testing | Staging     | Testing new features      |
| `dev-local-testing`     | Developer laptop testing    | Development | Local development         |
| `ci-cd-tests`           | Automated test suite        | CI/CD       | GitHub Actions, GitLab CI |
| `partner-integration`   | Third-party partner access  | Production  | Partner API access        |

**Benefits of Multiple Keys:**

* **Isolation:** If one key is compromised, only the associated service is affected
* **Accountability:** You can track which key is used for which requests
* **Auditability:** Different keys can have different activity logs
* **Easy Revocation:** Revoke a specific key without disrupting other services
* **Least Privilege:** Each key can be associated with specific monitoring and restrictions
* **Team Organization:** Different team members can manage different keys

**How to Create Multiple Keys:**

1. Log into your Pullbay account
2. Go to "Settings" → "API Keys"
3. Click "Generate New API Key"
4. Enter a descriptive name following your naming convention
5. Review the key details and copy it immediately
6. Store it securely in your secret manager or environment variable system
7. Test the key by making a test API request
8. Document the key's purpose and which service uses it

### Key Rotation Without Downtime

The ability to rotate keys without downtime is essential for production systems. Pullbay supports multiple active keys simultaneously, allowing you to rotate keys safely.

**The Zero-Downtime Rotation Process:**

```
Step 1: Generate New Key
  └─ New key is immediately active
  └─ Old key continues working during transition

Step 2: Update All Services
  └─ Deploy new key to production code/config
  └─ Services gradually transition to new key
  └─ Monitor that transition is complete

Step 3: Verify New Key Usage
  └─ Monitor API dashboard for requests using new key
  └─ Confirm all services are using the new key
  └─ Wait a few hours to ensure all instances have updated

Step 4: Revoke Old Key
  └─ Once confident all services use new key
  └─ Revoke the old key to prevent accidental usage
  └─ Rotation is complete

Rollback: If something goes wrong, the old key still works until revoked
```

**Step-by-Step Rotation Guide:**

{% stepper %}
{% step %}

#### Preparation

**Duration:** 30 minutes

1. Schedule a maintenance window (optional but recommended)
2. Notify your team that a key rotation is happening
3. Generate a new API key in your Pullbay account
4. Copy the new key and store it temporarily in a secure location
5. Prepare your deployment process for the new key
   {% endstep %}

{% step %}

#### Deployment

**Duration:** 30-60 minutes

1. Update your environment variables or secret manager with the new key
2. Deploy the updated configuration to all your services
3. For containerized deployments, restart containers with new config
4. For serverless, redeploy functions
5. For scheduled jobs, ensure the next scheduled run uses the new key
   {% endstep %}

{% step %}

#### Verification

**Duration:** 15-30 minutes

1. Make test requests to verify your services work with the new key
2. Monitor your API dashboard for new key requests
3. Check error rates and latencies—they should remain normal
4. Confirm all expected services are using the new key
5. Look for any failed requests or connection errors
   {% endstep %}

{% step %}

#### Monitoring

**Duration:** 30 minutes to several hours

1. Monitor your production metrics for the next 1-2 hours
2. Watch for any unexpected behavior or error spikes
3. If any service fails, you can temporarily revert to the old key
4. Once confident everything is working, proceed to cleanup
   {% endstep %}

{% step %}

#### Cleanup

**Duration:** Immediate

1. Review logs to confirm the old key is no longer being used
2. Revoke the old key in your Pullbay account
3. Archive the old key in your records
4. Update your key rotation documentation
5. Notify your team that rotation is complete
   {% endstep %}
   {% endstepper %}

**Example Rotation Timeline:**

```
08:00 AM - Schedule maintenance window and notify team
08:15 AM - Generate new API key in Pullbay account
08:20 AM - Update environment variables in secret manager
08:25 AM - Deploy new configuration to production
08:45 AM - Verify all services are using new key
09:00 AM - Confirm no errors or issues
09:05 AM - Revoke old API key
09:10 AM - Document completion and notify team
```

**Important Notes:**

* Pullbay supports multiple active keys, so there's no cutoff moment—both keys work during transition
* You can take your time verifying everything works before revoking the old key
* If something goes wrong during deployment, the old key still works as a fallback
* Always keep the old key active until you're 100% certain the new key is in use everywhere

### Key Naming Conventions

A systematic naming convention makes it easy to identify which key serves which purpose and simplifies management.

**Recommended Naming Pattern:**

```
{environment}-{service/app}-{description}
```

**Example Keys:**

| Key Name               | Environment | Service        | Description      | Usage                     |
| ---------------------- | ----------- | -------------- | ---------------- | ------------------------- |
| `prod-api-server`      | Production  | API Server     | Main application | Real-time data requests   |
| `prod-data-warehouse`  | Production  | Data Warehouse | ETL job          | Scheduled data collection |
| `staging-web-app`      | Staging     | Web App        | Testing          | Feature testing           |
| `staging-notebooks`    | Staging     | Notebooks      | Analysis         | Data science analysis     |
| `dev-local-testing`    | Development | Local Dev      | Personal         | Developer laptop          |
| `ci-cd-github-actions` | CI/CD       | GitHub         | Automated tests  | Test suite runner         |
| `ci-cd-gitlab-ci`      | CI/CD       | GitLab         | Automated tests  | Alternative CI/CD         |
| `partner-acme-corp`    | Production  | Partner        | Third-party      | Partner integration       |
| `archive-2025-retired` | Archive     | Archive        | Rotated out      | Old key (to be deleted)   |

**Benefits of This Convention:**

* **At a glance:** You know the environment, service, and purpose
* **Searchability:** Easy to filter keys in your dashboard
* **Documentation:** The name serves as self-documentation
* **Security:** Clear ownership and responsibility for each key
* **Automation:** Scripts can parse key names to determine handling

**Naming Rules:**

* Use lowercase letters, numbers, and hyphens only
* Avoid special characters that may cause parsing issues
* Keep names under 50 characters for readability
* Include environment as first component for clarity
* Include service/app as second component
* Add descriptive suffix if needed for clarity
* Don't include the actual key value in the name (obviously)
* Don't include personally identifiable information in key names

## Activity Logging and Monitoring

Pullbay logs all API requests made with your keys. This activity log is crucial for security monitoring and troubleshooting.

### What Pullbay Logs

For each API request, Pullbay records:

| Field                | Details                                           |
| -------------------- | ------------------------------------------------- |
| **Timestamp**        | When the request was made (UTC)                   |
| **API Key**          | Which key was used (last 8 characters visible)    |
| **Endpoint**         | Which data endpoint was requested                 |
| **Request Status**   | Success, rate limit, authentication error, etc.   |
| **Results Returned** | Number of results in the response                 |
| **Credits Consumed** | How many credits the request cost                 |
| **User Agent**       | Your application's HTTP user agent string         |
| **Request ID**       | Unique identifier for the request (for debugging) |
| **Response Time**    | How long the request took to complete             |

### Accessing Activity Logs

**In the Pullbay Dashboard:**

1. Log into your Pullbay account
2. Go to "Monitoring" → "Activity Log"
3. Filter by:
   * Date range (today, last 7 days, last 30 days, custom)
   * Specific API key
   * Endpoint
   * Status (success, error, rate limited)
4. Export activity as CSV for analysis
5. Set up alerts for unusual patterns

**Programmatic Access:**

Coming soon—API endpoint for querying activity logs programmatically.

### Detecting Suspicious Activity

Watch your activity logs for these red flags:

| Suspicious Pattern                                 | Possible Cause                                | Action                                     |
| -------------------------------------------------- | --------------------------------------------- | ------------------------------------------ |
| Sudden spike in request volume                     | Compromised key or runaway code               | Check the key and application immediately  |
| Requests from unexpected geographic locations      | Key leaked to unauthorized party              | Revoke key if not explained                |
| Failed authentication errors in high volume        | Attacker trying random keys                   | Monitor for additional signs of compromise |
| Requests to endpoints your app doesn't use         | Attacker exploring your data                  | Investigate what data was accessed         |
| Usage at unusual times (middle of night, weekends) | Unauthorized access                           | Compare to your known usage patterns       |
| Requests from unexpected user agents               | Unauthorized application or script            | Identify the user agent and take action    |
| Activity on a "disabled" or "rotated" key          | Key not properly revoked or stored insecurely | Revoke the key immediately                 |

**Example:** If your application only requests app store reviews but you see requests to a different endpoint, that's a sign your key may be compromised.

### Setting Up Monitoring Alerts

In your Pullbay account, configure alerts for:

* **Credit threshold alerts:** Notify you when credits fall below a certain amount
* **Unusual activity alerts:** Sudden spike in request volume above your baseline
* **Key compromise alerts:** Failed authentication attempts, requests outside expected hours
* **Rate limit alerts:** When approaching or hitting rate limits
* **Error rate alerts:** Spike in failed requests

Configure these in Settings → Alerts → API Key Monitoring.

## Infrastructure Recommendations by Deployment Level

Secure API key management depends on your deployment context. Here are recommendations for different scenarios.

### Production Environment — Secret Manager (Recommended)

For production systems, use a dedicated secret management service:

**AWS Secrets Manager:**

```python
import boto3
import json

client = boto3.client('secretsmanager', region_name='us-east-1')

try:
    secret = client.get_secret_value(SecretId='pullbay-api-key-prod')
    api_key = json.loads(secret['SecretString'])['api_key']
except Exception as e:
    print(f"Error retrieving secret: {e}")
    raise

# Use api_key for Pullbay requests
```

**Google Secret Manager:**

```python
from google.cloud import secretmanager

def access_secret_version(secret_id, version_id="latest"):
    client = secretmanager.SecretManagerServiceClient()
    name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}"
    response = client.access_secret_version(request={"name": name})
    return response.payload.data.decode("UTF-8")

api_key = access_secret_version("pullbay-api-key-prod")
```

**HashiCorp Vault:**

```python
import hvac

client = hvac.Client(url='https://vault.example.com', token='s.xxxxxxxx')
secret = client.secrets.kv.read_secret_version(path='pullbay/prod')
api_key = secret['data']['data']['api_key']
```

**Azure Key Vault:**

```python
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient

credential = DefaultAzureCredential()
client = SecretClient(vault_url="https://my-vault.vault.azure.net/", credential=credential)
api_key = client.get_secret("pullbay-api-key-prod").value
```

**Benefits:**

* Centralized key management and rotation
* Detailed audit logs of all key access
* Encryption at rest and in transit
* Fine-grained access control (who can access which secrets)
* Automatic key rotation support
* Integration with your infrastructure as code
* Audit logging and access control policies

**When to Use:**

* All production deployments
* Any environment with sensitive data
* Enterprise deployments requiring audit trails
* Regulated industries (finance, healthcare, etc.)

### Simple Setups — Environment Variables

For development and simple deployments, environment variables offer a good balance of simplicity and security.

**Store API Key as Environment Variable:**

```bash
# .env file (NEVER commit to version control)
PULLBAY_API_KEY=api_key_xxxxxxxxxxxxxxxx

# Or set directly in shell
export PULLBAY_API_KEY="api_key_xxxxxxxxxxxxxxxx"
```

**Load in Python:**

```python
import os
from dotenv import load_dotenv  # pip install python-dotenv

# Load from .env file in development
load_dotenv()

api_key = os.getenv('PULLBAY_API_KEY')
if not api_key:
    raise ValueError("PULLBAY_API_KEY environment variable not set")

# Use api_key for Pullbay requests
```

**Load in Node.js:**

```javascript
require('dotenv').config();

const apiKey = process.env.PULLBAY_API_KEY;
if (!apiKey) {
    throw new Error('PULLBAY_API_KEY environment variable not set');
}

// Use apiKey for Pullbay requests
```

**Load in PHP:**

```php
<?php
require 'vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();

$apiKey = $_ENV['PULLBAY_API_KEY'];
if (!$apiKey) {
    throw new Exception('PULLBAY_API_KEY environment variable not set');
}

// Use $apiKey for Pullbay requests
```

**Key Rules for Environment Variables:**

1. **NEVER commit .env files to version control**

   ```bash
   # Add to .gitignore
   .env
   .env.local
   .env.*.local
   ```
2. **Create .env.example with placeholders**

   ```bash
   # .env.example (SAFE to commit)
   PULLBAY_API_KEY=your_api_key_here
   PULLBAY_BASE_URL=https://api.pullbay.io
   ```
3. **Document environment variables in your README**

   ````markdown
   ## Configuration

   Create a .env file based on .env.example:
   ```bash
   cp .env.example .env
   ````

   Then add your Pullbay API key:

   ```bash
   PULLBAY_API_KEY=api_key_xxxxxxxxxxxxxxxx
   ```
4. **Load early in your application startup**
   * Load environment variables before using them
   * Fail fast if required variables are missing
   * Validate that the API key format is correct
5. **Use environment-specific variables**

   ```bash
   # Development
   PULLBAY_API_KEY_DEV=dev_key_xxxxxxx

   # Staging
   PULLBAY_API_KEY_STAGING=staging_key_xxxxxxx

   # Production
   PULLBAY_API_KEY_PROD=prod_key_xxxxxxx
   ```

**When to Use Environment Variables:**

* Development and testing
* Simple applications with few dependencies
* Containerized deployments (Docker, Kubernetes)
* Simple cloud deployments (Heroku, AWS Lambda)
* Any scenario where a secret manager is not available

### CI/CD Pipelines — Platform Secret Storage

CI/CD platforms (GitHub Actions, GitLab CI, Jenkins, etc.) have built-in secret management.

**GitHub Actions:**

```yaml
# .github/workflows/pullbay-job.yml
name: Pullbay Data Pipeline

on:
  schedule:
    - cron: '0 2 * * *'  # Daily at 2 AM UTC

jobs:
  fetch-data:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Fetch data from Pullbay
        env:
          PULLBAY_API_KEY: ${{ secrets.PULLBAY_API_KEY_PROD }}
        run: python scripts/fetch_reviews.py
```

**To Add the Secret to GitHub:**

1. Go to your GitHub repository
2. Settings → Secrets and variables → Actions
3. Click "New repository secret"
4. Name: `PULLBAY_API_KEY_PROD`
5. Value: (paste your API key)
6. Click "Add secret"

**GitLab CI:**

```yaml
# .gitlab-ci.yml
fetch_data:
  stage: scheduled
  script:
    - pip install -r requirements.txt
    - python scripts/fetch_reviews.py
  variables:
    PULLBAY_API_KEY: $PULLBAY_API_KEY_PROD
  only:
    - schedules
```

**To Add the Secret to GitLab:**

1. Go to your GitLab project
2. Settings → CI/CD → Variables
3. Click "Add variable"
4. Key: `PULLBAY_API_KEY_PROD`
5. Value: (paste your API key)
6. Check "Protect variable" to limit visibility
7. Click "Add variable"

**Jenkins:**

```groovy
pipeline {
    agent any

    stages {
        stage('Fetch Data') {
            steps {
                withCredentials([string(credentialsId: 'pullbay-api-key-prod', variable: 'PULLBAY_API_KEY')]) {
                    sh '''
                        pip install -r requirements.txt
                        python scripts/fetch_reviews.py
                    '''
                }
            }
        }
    }
}
```

**CI/CD Secret Management Best Practices:**

| Practice                                      | Benefit                                      |
| --------------------------------------------- | -------------------------------------------- |
| Use separate secrets per environment          | Limits exposure if one secret is compromised |
| Mark secrets as "protected" or "masked"       | Prevents accidental exposure in logs         |
| Limit secret access to specific jobs/branches | Reduces unauthorized access risk             |
| Rotate CI/CD secrets regularly                | Prevents long-term exposure                  |
| Use short-lived credentials if available      | Reduces the window of exposure               |
| Audit secret access logs                      | Detects unauthorized access attempts         |
| Don't echo secrets in logs                    | Prevents accidental exposure                 |
| Clean up artifacts that may contain secrets   | Prevents leakage through build artifacts     |

**Important:** Even though secrets are protected, never trust that they can't be exposed. Always assume CI/CD environments can be compromised and use the principle of least privilege.

## Code Examples — Secure Key Loading

Here are production-ready examples for loading your API key securely in common languages.

### Python

**With environment variables:**

```python
import os
from pullbay import PullbayClient

def get_api_key():
    """Load API key from environment variable safely."""
    api_key = os.getenv('PULLBAY_API_KEY')
    if not api_key:
        raise ValueError(
            "PULLBAY_API_KEY environment variable not set. "
            "Please set it before running this script."
        )

    # Basic validation
    if not api_key.startswith('api_key_'):
        raise ValueError(
            "Invalid API key format. API keys should start with 'api_key_'"
        )

    return api_key

def main():
    """Fetch data from Pullbay."""
    api_key = get_api_key()
    client = PullbayClient(api_key=api_key)

    # Make requests
    reviews = client.app_store_reviews(limit=10)
    print(f"Fetched {len(reviews)} reviews")

if __name__ == '__main__':
    main()
```

**With AWS Secrets Manager:**

```python
import boto3
import json
from pullbay import PullbayClient

def get_api_key_from_secrets_manager(secret_name='pullbay-api-key-prod'):
    """Load API key from AWS Secrets Manager."""
    client = boto3.client('secretsmanager', region_name='us-east-1')

    try:
        response = client.get_secret_value(SecretId=secret_name)
        secret = json.loads(response['SecretString'])
        return secret['api_key']
    except Exception as e:
        raise RuntimeError(f"Failed to retrieve API key from Secrets Manager: {e}")

def main():
    api_key = get_api_key_from_secrets_manager()
    client = PullbayClient(api_key=api_key)
    reviews = client.app_store_reviews(limit=10)
    print(f"Fetched {len(reviews)} reviews")

if __name__ == '__main__':
    main()
```

### Node.js

**With environment variables:**

```javascript
const axios = require('axios');

function getApiKey() {
    const apiKey = process.env.PULLBAY_API_KEY;
    if (!apiKey) {
        throw new Error(
            'PULLBAY_API_KEY environment variable not set. ' +
            'Please set it before running this script.'
        );
    }

    // Basic validation
    if (!apiKey.startsWith('api_key_')) {
        throw new Error(
            'Invalid API key format. API keys should start with "api_key_"'
        );
    }

    return apiKey;
}

async function fetchData() {
    const apiKey = getApiKey();

    const response = await axios.get('https://api.pullbay.io/app-store/reviews', {
        headers: {
            'Authorization': `Bearer ${apiKey}`,
            'User-Agent': 'my-app/1.0'
        },
        params: {
            limit: 10
        }
    });

    console.log(`Fetched ${response.data.results.length} reviews`);
}

fetchData().catch(error => {
    console.error('Error:', error.message);
    process.exit(1);
});
```

**With AWS Secrets Manager:**

```javascript
const AWS = require('aws-sdk');
const axios = require('axios');

const secretsManager = new AWS.SecretsManager({ region: 'us-east-1' });

async function getApiKeyFromSecretsManager(secretName = 'pullbay-api-key-prod') {
    try {
        const data = await secretsManager.getSecretValue({ SecretId: secretName }).promise();
        const secret = JSON.parse(data.SecretString);
        return secret.api_key;
    } catch (error) {
        throw new Error(`Failed to retrieve API key from Secrets Manager: ${error.message}`);
    }
}

async function fetchData() {
    const apiKey = await getApiKeyFromSecretsManager();

    const response = await axios.get('https://api.pullbay.io/app-store/reviews', {
        headers: {
            'Authorization': `Bearer ${apiKey}`,
            'User-Agent': 'my-app/1.0'
        },
        params: {
            limit: 10
        }
    });

    console.log(`Fetched ${response.data.results.length} reviews`);
}

fetchData().catch(error => {
    console.error('Error:', error.message);
    process.exit(1);
});
```

### PHP

**With environment variables:**

```php
<?php

class PullbayClient {
    private $apiKey;
    private $baseUrl = 'https://api.pullbay.io';

    public function __construct() {
        $this->apiKey = $this->getApiKey();
    }

    private function getApiKey() {
        $apiKey = $_ENV['PULLBAY_API_KEY'] ?? getenv('PULLBAY_API_KEY');

        if (!$apiKey) {
            throw new Exception(
                'PULLBAY_API_KEY environment variable not set. ' .
                'Please set it before running this script.'
            );
        }

        // Basic validation
        if (strpos($apiKey, 'api_key_') !== 0) {
            throw new Exception(
                'Invalid API key format. API keys should start with "api_key_"'
            );
        }

        return $apiKey;
    }

    public function fetchReviews($limit = 10) {
        $url = $this->baseUrl . '/app-store/reviews';

        $context = stream_context_create([
            'http' => [
                'method' => 'GET',
                'header' => [
                    'Authorization: Bearer ' . $this->apiKey,
                    'User-Agent: my-app/1.0'
                ]
            ]
        ]);

        $response = @file_get_contents($url . '?limit=' . $limit, false, $context);

        if ($response === false) {
            throw new Exception('Failed to fetch data from Pullbay API');
        }

        return json_decode($response, true);
    }
}

try {
    $client = new PullbayClient();
    $data = $client->fetchReviews(10);
    echo "Fetched " . count($data['results']) . " reviews\n";
} catch (Exception $e) {
    echo "Error: " . $e->getMessage() . "\n";
    exit(1);
}
```

**With Google Cloud Secret Manager:**

```php
<?php

use Google\Cloud\SecretManager\V1\SecretManagerServiceClient;

class PullbayClient {
    private $apiKey;
    private $baseUrl = 'https://api.pullbay.io';

    public function __construct($projectId) {
        $this->apiKey = $this->getApiKeyFromSecrets($projectId);
    }

    private function getApiKeyFromSecrets($projectId) {
        try {
            $client = new SecretManagerServiceClient();
            $name = $client->secretVersionName(
                $projectId,
                'pullbay-api-key-prod',
                'latest'
            );

            $response = $client->accessSecretVersion(['name' => $name]);
            $secret = json_decode($response->getPayload()->getData(), true);
            return $secret['api_key'];
        } catch (Exception $e) {
            throw new Exception(
                'Failed to retrieve API key from Secret Manager: ' . $e->getMessage()
            );
        }
    }

    public function fetchReviews($limit = 10) {
        // ... (same as previous example)
    }
}
```

## Frequently Asked Questions

<details>

<summary>How often should I rotate my API key?</summary>

**Recommendation:** Every 90 days, or immediately if exposed.

Industry best practices suggest rotating credentials every 90 days. However, you should rotate immediately if:

* The key has been exposed or compromised
* You've moved it to a new environment
* A team member with access to the key leaves your organization
* You suspect unauthorized access
* As part of a security incident response

Pullbay's zero-downtime rotation feature (create new → verify → revoke) makes frequent rotation painless.

</details>

<details>

<summary>What should I do if my key is leaked in a public repository?</summary>

**Immediate actions:**

1. **Revoke the key immediately** in your Pullbay account
2. **Contact GitHub/GitLab** to report the exposed secret
   * GitHub has a secret scanning feature that may already have detected it
   * You can manually report it via their security tools
3. **Create a new API key** and deploy it to all systems
4. **Force-push** to remove the secret from Git history (if possible):

   ```bash
   # Install git-filter-repo if needed
   pip install git-filter-repo

   # Remove the file from all history
   git filter-repo --path .env

   # Force push
   git push --force-with-lease
   ```
5. **Review your usage logs** for any unauthorized requests
6. **Monitor your account** for suspicious activity

**Prevention for the future:**

* Use a secret scanning tool in your CI/CD (e.g., GitGuardian, Snyk)
* Add a pre-commit hook to catch secrets before they're committed:

  ```bash
  pip install detect-secrets
  detect-secrets scan > .secrets.baseline
  ```
* Use `.gitignore` aggressively for sensitive files
* Educate your team about secret management

</details>

<details>

<summary>Can I restrict my API key to specific IP addresses?</summary>

**Current Status:** Not yet, but it's on our roadmap.

IP restriction would add an extra layer of security. In the meantime:

* Use separate keys for different environments and services
* Employ zero-trust network access (firewall rules at the infrastructure level)
* Use VPNs or private networks for accessing the Pullbay API
* Monitor your usage logs for requests from unexpected locations
* Consider API gateway solutions that can implement IP-based throttling

</details>

<details>

<summary>Can I see which team members have accessed which API keys?</summary>

**Current Status:** API key access logs are available in your Pullbay account.

You can view:

* When each key was created and last used
* Activity logs showing requests made by each key
* Failed authentication attempts

However, we don't currently track which team members accessed the secret manager or application code. This depends on your infrastructure:

* **AWS Secrets Manager:** CloudTrail logs show who accessed secrets
* **GitHub Secrets:** Audit logs show which workflows accessed secrets
* **Azure Key Vault:** Audit logs track all secret access

Check your infrastructure provider's audit logs for this visibility.

</details>

<details>

<summary>What if I accidentally use the same key in multiple environments?</summary>

**You should immediately rotate that key.**

Here's why: If one environment is compromised, all environments using that key are at risk.

**Steps to fix:**

1. Identify all systems using the key
2. Create separate keys for each environment/system
3. Deploy the new keys to each system
4. Verify each system is working with its new key
5. Revoke the shared key

This is why we recommend separate keys per environment from the start.

</details>

<details>

<summary>How do I know if my API key has been compromised?</summary>

Look for these signs in your activity logs:

* **Unusual request patterns:** Requests at times your app doesn't normally run
* **Unexpected endpoints:** Requests to data your app doesn't need
* **Geographic anomalies:** Requests from locations you don't operate in
* **Spike in failed authentication:** Someone trying random keys
* **Unexpected credit consumption:** Requests you didn't make

If you notice any of these patterns:

1. Check your application logs to see if the traffic came from your systems
2. If it didn't, revoke the key immediately
3. Investigate what data was accessed
4. Create a new key and redeploy
5. Review your security practices

</details>

<details>

<summary>Should I share my API key with partners or third parties?</summary>

**No—create a separate key for each partner instead.**

Here's why:

* **Isolation:** If a partner's system is compromised, only their key is affected
* **Audit trail:** You can see exactly which partner made which requests
* **Easy revocation:** You can disable a partner's access without affecting others
* **Rate limiting:** You can apply different limits per partner
* **Accountability:** Requests are clearly attributed to specific partners

**Instead:**

1. Create a new API key specifically for the partner
2. Document which key belongs to which partner
3. Share only that key (not your main key)
4. Monitor that key's activity closely
5. Revoke the key immediately if the partnership ends

</details>

<details>

<summary>How should I handle API keys for contractors or temporary team members?</summary>

**Create temporary keys and revoke them immediately when they leave.**

**Process:**

1. Create a key specifically for the contractor (e.g., `contractor-john-q3-2026`)
2. Set an expiration reminder for when the contractor's engagement ends
3. Share the key only with that contractor
4. Monitor the key's activity to ensure proper use
5. The day the contractor leaves, revoke the key in your Pullbay account
6. Verify no more requests are made with that key

This ensures former contractors can't access your data.

</details>

<details>

<summary>What credentials should I use in automated tests?</summary>

**Use a dedicated test API key** with a lower credit balance if possible.

**Approach:**

1. Create a test key (e.g., `ci-testing` or `dev-local-testing`)
2. Use that key in your test suite only
3. Keep the test key's credit balance low to limit damage if tests go wrong
4. Store the test key in your CI/CD secrets, not in code
5. Rotate test keys every few months
6. Monitor test key activity for unusual patterns

This isolates your test environment from production and prevents runaway tests from consuming all your credits.

</details>

***

**Have questions about API key security?** Contact Pullbay support or read our [Getting Started guide](broken://pages/dc418bd608f33fb865552fe0d19fda860fc07b06) for more information.


# Cost Optimization (REVISIT)

### Overview

Pullbay's credit-based pricing model rewards efficient API usage. This guide provides 7 proven optimization strategies with code examples to reduce your monthly credit costs by 50-90%. Whether you're just starting or scaling to thousands of requests, these techniques apply to every usage pattern.

***

### Understand Your Usage First

Before optimizing, understand where you're spending credits. Most of Pullbay's dashboard provides a usage breakdown by endpoint.

**Steps to analyze your usage:**

1. Log into your Pullbay dashboard
2. Navigate to **Usage & Analytics**
3. View breakdown by endpoint, date range, and request volume
4. Identify top-spending endpoints (usually reviews, search results, business info)
5. Look for spikes or anomalies

**Questions to ask:**

* Which endpoints consume the most credits?
* Are there spikes at certain times?
* How many requests do you make per day?
* How many pages per request do you typically fetch?

Once you identify patterns, apply the optimization strategies below to your top-spending endpoints first.

***

### 7 Optimization Strategies

#### Strategy 1: Fetch Only What You Need (Best ROI)

**Impact: 50-90% credit savings**

This single strategy delivers the highest return on investment. Stop fetching data you don't use.

**Problem: Over-fetching**

```python
# BAD: Fetches ALL 5,000 reviews when you only display 50
all_reviews = []
for review in paginator.paginate("reviews", params={"business_id": "12345"}):
    all_reviews.append(review)

# Later: use only the first 50 for your dashboard
recent_reviews = all_reviews[:50]

print(f"Fetched {len(all_reviews)}, but used only {len(recent_reviews)}")
# Output: Fetched 5000, but used only 50 (99% wasted credits!)
```

**Solution: Fetch with a Limit**

```python
# GOOD: Stop after 50 reviews
recent_reviews = []
for review in paginator.paginate(
    "reviews",
    params={"business_id": "12345"}
):
    recent_reviews.append(review)

    if len(recent_reviews) >= 50:
        break  # Stop here!

print(f"Fetched exactly {len(recent_reviews)} reviews")
# Output: Fetched exactly 50 reviews (100% efficient)
```

Or use built-in `max_results` parameter:

```python
# BEST: Uses built-in limit (stops at API level)
recent_reviews = list(paginator.paginate(
    "reviews",
    params={"business_id": "12345"},
    max_results=50  # API stops fetching after 50
))
```

**Real-world Savings Example**

| Scenario                     | Without Limit        | With Limit          | Savings |
| ---------------------------- | -------------------- | ------------------- | ------- |
| Dashboard display (50 items) | 5,000 items fetched  | 50 items fetched    | 99%     |
| Weekly export (500 items)    | 10,000 items fetched | 500 items fetched   | 95%     |
| Monthly report (1,000 items) | 2,000 items fetched  | 1,000 items fetched | 50%     |

**Annual credit example:**

* 10 apps × 40 pages × 30 days = **12,000 credits/month** (full fetch)
* With 50-item limit per app (from 5,000 items): **240 credits/month**
* **Savings: \~11,760 credits/month (98%)**

**When to use this strategy:**

* Displaying top-N results (top 10 reviews, top 50 search results)
* Sampling data (get 100 items to test quality)
* Pagination in web applications (show 50 per page, fetch more on demand)
* Any time you know your maximum needed result count

***

#### Strategy 2: Cache Responses (50% Credit Savings with 50% Hit Rate)

**Impact: 30-70% credit savings**

Cache API responses so repeated requests don't consume credits.

**Simple TTL-based Caching**

```python
import json
import time
import os
from datetime import datetime, timedelta

class ReviewCache:
    def __init__(self, cache_dir="./cache", ttl_seconds=3600):
        self.cache_dir = cache_dir
        self.ttl = ttl_seconds
        os.makedirs(cache_dir, exist_ok=True)

    def _cache_key(self, business_id):
        """Generate cache filename."""
        return os.path.join(self.cache_dir, f"reviews_{business_id}.json")

    def get(self, business_id):
        """Get cached reviews if valid."""
        cache_file = self._cache_key(business_id)

        if not os.path.exists(cache_file):
            return None

        # Check if cache is still fresh
        file_age = time.time() - os.path.getmtime(cache_file)
        if file_age > self.ttl:
            return None  # Cache expired

        with open(cache_file) as f:
            return json.load(f)

    def set(self, business_id, reviews):
        """Cache reviews."""
        cache_file = self._cache_key(business_id)

        with open(cache_file, 'w') as f:
            json.dump(reviews, f)

    def get_or_fetch(self, business_id):
        """Get from cache or fetch from API."""
        # Try cache first
        cached = self.get(business_id)
        if cached:
            print(f"Cache hit: {business_id}")
            return cached

        # Fetch from API
        print(f"Cache miss: {business_id}, fetching from API")
        reviews = managed_paginator.fetch_all(
            "reviews",
            params={"business_id": business_id}
        )

        # Store in cache
        self.set(business_id, reviews)
        return reviews

# Usage
cache = ReviewCache(ttl_seconds=3600)  # 1-hour cache

# First call: fetches from API (costs 100 credits)
reviews_1 = cache.get_or_fetch("12345")

# Second call (within 1 hour): serves from cache (costs 0 credits)
reviews_2 = cache.get_or_fetch("12345")

print(f"Second call saved 100 credits!")
```

**Savings Calculation**

**Assumptions:**

* Fetch 100 reviews per business = 100 credits
* Average 50% cache hit rate
* 100 requests/month

```
Without caching: 100 requests × 100 credits = 10,000 credits/month
With caching (50% hits): 50 requests × 100 credits = 5,000 credits/month
Savings: 50% (5,000 credits saved)
```

**When to use this strategy:**

* Repeated requests for the same business
* Data that doesn't change frequently
* User dashboards that refresh multiple times daily
* Batch jobs that re-query the same data

**Choose TTL based on data freshness:**

```python
# Real-time data: 5-minute cache
cache = ReviewCache(ttl_seconds=300)

# Hourly dashboard: 1-hour cache
cache = ReviewCache(ttl_seconds=3600)

# Daily batch: 24-hour cache
cache = ReviewCache(ttl_seconds=86400)

# Monthly reports: 7-day cache
cache = ReviewCache(ttl_seconds=604800)
```

***

#### Strategy 3: Batch Related Requests

**Impact: 30-60% credit savings**

Combine multiple related queries into fewer API calls using Managed pagination and smart filtering.

**Problem: Multiple Separate Requests**

```python
# BAD: 3 separate requests to fetch reviews for 3 businesses
business_ids = ["12345", "12346", "12347"]

for business_id in business_ids:
    # Each call is separate (no batch)
    reviews = managed_paginator.fetch_all(
        "reviews",
        params={"business_id": business_id}
    )
    # 100 credits × 3 = 300 credits total
```

**Solution: Batch with Filtering**

```python
# GOOD: Single request with batch processing
# Fetch reviews for all businesses, filter client-side
all_reviews = managed_paginator.fetch_all(
    "reviews",
    params={
        # Batch parameter: fetch for multiple IDs
        "business_ids": ["12345", "12346", "12347"],
        # Limit to recent reviews only
        "created_after": "2024-01-01"
    }
)

# Process by business
by_business = {}
for review in all_reviews:
    business_id = review['business_id']
    if business_id not in by_business:
        by_business[business_id] = []
    by_business[business_id].append(review)

# ~200 credits (vs 300 with 3 separate requests)
# Savings: 33%
```

**Batch + Cache Pattern**

```python
class BatchedReviewFetcher:
    def __init__(self, paginator, cache, batch_size=10):
        self.paginator = paginator
        self.cache = cache
        self.batch_size = batch_size

    def fetch_reviews_batch(self, business_ids):
        """Fetch reviews for multiple businesses in batches."""

        all_results = {}

        for i in range(0, len(business_ids), self.batch_size):
            batch = business_ids[i:i+self.batch_size]

            # Try cache first
            cached_batch = []
            to_fetch = []

            for bid in batch:
                cached = self.cache.get(bid)
                if cached:
                    all_results[bid] = cached
                else:
                    to_fetch.append(bid)

            # Fetch uncached items in batch
            if to_fetch:
                batch_reviews = self.paginator.fetch_all(
                    "reviews",
                    params={"business_ids": to_fetch}
                )

                # Separate by business and cache
                for review in batch_reviews:
                    bid = review['business_id']
                    if bid not in all_results:
                        all_results[bid] = []
                    all_results[bid].append(review)

                    # Cache individual business results
                    self.cache.set(bid, all_results[bid])

        return all_results

# Usage
fetcher = BatchedReviewFetcher(managed_paginator, cache, batch_size=10)

business_ids = ["12345", "12346", "12347", "12348", "12349"]
reviews_by_business = fetcher.fetch_reviews_batch(business_ids)

# Only fetch what's not cached
# If 3 of 5 are cached: 2 API calls instead of 5
# Savings: 60%
```

***

#### Strategy 4: Avoid Duplicate Requests

**Impact: 20-50% credit savings**

Deduplicate requests using a centralized service class.

**Problem: Duplicate Requests**

```python
# BAD: Multiple parts of code fetch the same data
class DashboardService:
    def get_reviews(self, business_id):
        return managed_paginator.fetch_all("reviews", {"business_id": business_id})

class ReportService:
    def generate_report(self, business_id):
        # Same request again!
        reviews = managed_paginator.fetch_all("reviews", {"business_id": business_id})

class AlertService:
    def check_alerts(self, business_id):
        # Same request AGAIN!
        reviews = managed_paginator.fetch_all("reviews", {"business_id": business_id})

# Same business data fetched 3 times (300 credits)
```

**Solution: Centralized Service with Caching**

```python
class ReviewService:
    """Centralized service for all review operations."""

    def __init__(self, paginator, cache_ttl=3600):
        self.paginator = paginator
        self.cache = ReviewCache(ttl_seconds=cache_ttl)

    def get_reviews(self, business_id, limit=None):
        """Get reviews for a business (cached)."""
        cached = self.cache.get(business_id)
        if cached:
            reviews = cached
        else:
            reviews = self.paginator.fetch_all(
                "reviews",
                params={"business_id": business_id}
            )
            self.cache.set(business_id, reviews)

        if limit:
            return reviews[:limit]
        return reviews

# Usage: Single service instance used everywhere
review_service = ReviewService(managed_paginator)

class DashboardService:
    def get_reviews(self, business_id):
        return review_service.get_reviews(business_id, limit=50)

class ReportService:
    def generate_report(self, business_id):
        return review_service.get_reviews(business_id)

class AlertService:
    def check_alerts(self, business_id):
        return review_service.get_reviews(business_id)

# Same data requested 3 times, fetched only once (100 credits)
# Savings: 67% (200 of 300 credits saved)
```

**Detect Duplicate Requests**

```python
import logging

class TrackedReviewService:
    def __init__(self, paginator):
        self.paginator = paginator
        self.cache = ReviewCache()
        self.request_count = {}
        self.logger = logging.getLogger(__name__)

    def get_reviews(self, business_id):
        """Track duplicate requests."""

        # Count requests
        self.request_count[business_id] = self.request_count.get(business_id, 0) + 1

        # Warn on duplicates
        if self.request_count[business_id] > 1:
            self.logger.warning(
                f"Duplicate request for business {business_id} "
                f"(call #{self.request_count[business_id]})"
            )

        return self.cache.get_or_fetch(business_id)

    def print_stats(self):
        """Print request statistics."""
        duplicates = sum(1 for c in self.request_count.values() if c > 1)
        total_requests = sum(self.request_count.values())
        potential_savings = sum(c - 1 for c in self.request_count.values()) * 100

        print(f"Total requests: {total_requests}")
        print(f"Unique businesses: {len(self.request_count)}")
        print(f"Duplicates: {duplicates}")
        print(f"Potential credit savings: {potential_savings} credits")

# Usage
service = TrackedReviewService(managed_paginator)
service.get_reviews("12345")
service.get_reviews("12345")  # Duplicate!
service.get_reviews("12346")

service.print_stats()
# Output:
# Total requests: 3
# Unique businesses: 2
# Duplicates: 1
# Potential credit savings: 100 credits
```

***

#### Strategy 5: Schedule Smart (Match Frequency to Data Change Rate)

**Impact: 40-80% credit savings**

Fetch data only as frequently as it changes. Over-fetching static data wastes credits.

**Fetch Frequency Strategy**

```python
import schedule
import time
from datetime import datetime

class SmartScheduler:
    def __init__(self, review_service):
        self.service = review_service

    def schedule_optimized(self):
        """Schedule fetches based on data change rate."""

        # Reviews change frequently: check multiple times/day
        schedule.every(2).hours.do(
            self.fetch_and_cache,
            endpoint="reviews",
            business_id="12345",
            reason="Reviews updated frequently"
        )

        # Search results change often: check multiple times/day
        schedule.every(6).hours.do(
            self.fetch_and_cache,
            endpoint="search",
            business_id="12345",
            reason="Search index updates"
        )

        # Business info changes rarely: check daily
        schedule.every().day.at("02:00").do(
            self.fetch_and_cache,
            endpoint="business_info",
            business_id="12345",
            reason="Business info updated daily"
        )

        # Photos rarely change: check weekly
        schedule.every().monday.at("03:00").do(
            self.fetch_and_cache,
            endpoint="photos",
            business_id="12345",
            reason="Photos updated weekly"
        )

        # Run scheduler
        while True:
            schedule.run_pending()
            time.sleep(60)

    def fetch_and_cache(self, endpoint, business_id, reason):
        """Fetch data and cache it."""
        print(f"[{datetime.now()}] Fetching {endpoint} - {reason}")
        self.service.fetch_and_cache(endpoint, business_id)

# Usage
scheduler = SmartScheduler(review_service)
scheduler.schedule_optimized()
```

**Frequency Recommendations by Data Type**

| Data Type      | Change Frequency | Recommended Check | Monthly Requests | Savings vs Every Hour |
| -------------- | ---------------- | ----------------- | ---------------- | --------------------- |
| Reviews        | Every few hours  | Every 2-3 hours   | 240              | 84%                   |
| Search results | Every 6-12 hours | Every 6 hours     | 120              | 92%                   |
| Business info  | Daily            | Once daily        | 30               | 98%                   |
| Photos         | Weekly           | Once weekly       | 4                | 99%                   |
| Categories     | Monthly          | Once monthly      | 1                | 99.7%                 |

**Example: Optimization Impact**

```
Reviews (change every 2 hours):
  - Bad: Check every hour = 24 × 30 = 720 requests/month
  - Good: Check every 2 hours = 12 × 30 = 360 requests/month
  - Savings: 50%

Business info (changes daily):
  - Bad: Check every 6 hours = 4 × 30 = 120 requests/month
  - Good: Check once daily = 1 × 30 = 30 requests/month
  - Savings: 75%

Combined monthly impact:
  - Bad: 840 requests = 840 credits
  - Good: 390 requests = 390 credits
  - Savings: 54%
```

***

#### Strategy 6: Use Test Keys for Development

**Impact: 100% savings on dev costs**

Never use production credits for development. Use test keys to prototype without cost.

**Setting Up Test Keys**

```python
import os
from dotenv import load_dotenv

load_dotenv()

# Production key: Real credits consumed
PROD_API_KEY = os.getenv("PULLBAY_API_KEY_PROD")

# Test key: No credits consumed, limited data
TEST_API_KEY = os.getenv("PULLBAY_API_KEY_TEST")

def get_client(use_production=False):
    """Get Pullbay client with appropriate key."""
    api_key = PROD_API_KEY if use_production else TEST_API_KEY

    return PullbayClient(api_key=api_key)

# Development
dev_client = get_client(use_production=False)

# Production
prod_client = get_client(use_production=True)
```

**Test Key Best Practices**

```python
class DevelopmentPullbay:
    """Use only in development/testing."""

    def __init__(self):
        self.client = PullbayClient(api_key=os.getenv("PULLBAY_API_KEY_TEST"))
        # Test keys have limits
        self.max_requests_per_hour = 100
        self.max_items_per_request = 50

    def fetch_reviews(self, business_id, limit=10):
        """Fetch limited reviews for testing."""
        return self.client.fetch_all(
            "reviews",
            params={
                "business_id": business_id,
                "max_results": limit  # Limit to 10 for fast testing
            }
        )

    def test_pagination(self, business_id):
        """Test pagination logic without high costs."""
        reviews = []
        cursor = None

        while len(reviews) < 50:  # Stop at 50 for testing
            page = self.client.paginate(
                "reviews",
                params={"business_id": business_id},
                cursor=cursor,
                limit=10
            )

            reviews.extend(page['data'])
            cursor = page['pagination'].get('next_cursor')

            if not cursor:
                break

        return reviews

# Usage
dev_client = DevelopmentPullbay()

# Fast iteration without cost
dev_reviews = dev_client.fetch_reviews("12345", limit=10)
print(f"Development: Fetched {len(dev_reviews)} reviews (0 credits used)")
```

**Cost Comparison**

```
Development with production key:
  - 10 days of testing
  - 100 requests/day, 100 items/request
  - Cost: 100,000 credits consumed

Development with test key:
  - Same work, same testing
  - Cost: 0 production credits consumed

Savings: 100,000 credits per development sprint
```

***

#### Strategy 7: Monitor and Alert (Prevent Runaway Spending)

**Impact: Prevents 100-1,000+ credit waste from bugs**

Set up monitoring to catch unusual usage patterns before they drain your account.

**Basic Alert System**

```python
import time
from datetime import datetime, timedelta

class CreditMonitor:
    def __init__(self, client, alert_thresholds=None):
        self.client = client
        self.thresholds = alert_thresholds or {
            'percent_50': 50,      # Alert at 50% of monthly budget
            'percent_75': 75,      # Critical at 75%
            'daily_spike': 2.0     # Alert if 2× higher than daily average
        }
        self.daily_history = []

    def check_and_alert(self):
        """Check usage and send alerts if needed."""
        usage = self.client.get_usage()

        # Alert 1: Percent of monthly budget
        percent_used = (usage['credits_used'] / usage['monthly_budget']) * 100

        if percent_used >= self.thresholds['percent_75']:
            self._send_alert(
                f"CRITICAL: {percent_used:.1f}% of monthly budget used",
                severity='critical'
            )
        elif percent_used >= self.thresholds['percent_50']:
            self._send_alert(
                f"WARNING: {percent_used:.1f}% of monthly budget used",
                severity='warning'
            )

        # Alert 2: Daily spending spike
        today_usage = usage['credits_today']
        self.daily_history.append((datetime.now(), today_usage))

        if len(self.daily_history) >= 7:
            avg_daily = sum(c for _, c in self.daily_history[-7:]) / 7
            spike_multiplier = today_usage / max(avg_daily, 1)

            if spike_multiplier >= self.thresholds['daily_spike']:
                self._send_alert(
                    f"Alert: Today's usage ({today_usage} credits) is "
                    f"{spike_multiplier:.1f}x daily average ({avg_daily:.0f})",
                    severity='warning'
                )

    def _send_alert(self, message, severity='info'):
        """Send alert via email/Slack/etc."""
        print(f"[{severity.upper()}] {message}")

        # TODO: Integrate with email/Slack/PagerDuty
        # send_email(alert_recipients, subject=f"Pullbay {severity}", body=message)
        # post_slack(message, channel=alerts_channel)

# Usage
monitor = CreditMonitor(
    client=pullbay_client,
    alert_thresholds={
        'percent_50': 50,
        'percent_75': 75,
        'daily_spike': 2.0
    }
)

# Run periodically (every hour)
schedule.every().hour.do(monitor.check_and_alert)
```

**Production Monitoring Checklist**

```python
class ProductionMonitoring:
    """Comprehensive monitoring for production."""

    @staticmethod
    def setup_alerts():
        """Set up all recommended alerts."""

        alerts = [
            {
                'name': '50% budget alert',
                'condition': 'credits_used > (monthly_budget * 0.5)',
                'recipients': ['ops@company.com']
            },
            {
                'name': '75% budget alert',
                'condition': 'credits_used > (monthly_budget * 0.75)',
                'recipients': ['ops@company.com', 'cto@company.com']
            },
            {
                'name': '2x daily spike',
                'condition': 'credits_today > (avg_daily_credits * 2)',
                'recipients': ['ops@company.com']
            },
            {
                'name': 'API errors spike',
                'condition': 'error_rate_1h > 5%',
                'recipients': ['ops@company.com']
            },
            {
                'name': 'Rate limit hits',
                'condition': 'rate_limit_hits_1h > 10',
                'recipients': ['ops@company.com']
            }
        ]

        for alert in alerts:
            print(f"Configured alert: {alert['name']}")
            print(f"  Recipients: {alert['recipients']}")

    @staticmethod
    def log_metrics():
        """Log detailed metrics."""
        metrics = {
            'timestamp': datetime.now().isoformat(),
            'credits_used_today': 0,
            'requests_made_today': 0,
            'avg_credits_per_request': 0,
            'top_endpoints': [],
            'error_rate': 0,
        }

        # Log to monitoring system (DataDog, New Relic, etc.)
        print(f"Metrics: {metrics}")
```

***

### Cost Estimation Formula

Estimate your monthly credit usage with this formula:

```
Monthly Credits = (Requests Per Endpoint) × (Pages Per Request) × (Frequency Per Month)

Example 1: Single Business, All Reviews
  Requests = 1
  Pages = 40 pages (at 50 items/page = 2,000 reviews)
  Frequency = 30 (once daily)
  Monthly Credits = 1 × 40 × 30 = 1,200 credits

Example 2: Multiple Apps (10 apps), Daily Reviews
  Reviews: 10 apps × 40 pages × 30 days = 12,000 credits/month

Example 3: With Optimization (Same Use Case, With Caching)
  Reviews (50% cache hits): 12,000 × 0.5 = 6,000 credits/month
  Savings: 6,000 credits/month (50%)
```

***

### Cost Estimation Spreadsheet

Create a spreadsheet to track and estimate costs:

```
| Endpoint | Freq | Pages | Requests/Mo | Credits/Request | Monthly Credits | Notes |
|----------|------|-------|-------------|-----------------|-----------------|-------|
| Reviews | Daily | 40 | 300 | 40 | 12,000 | 10 apps |
| Reviews (cached) | Daily | 40 | 150 | 40 | 6,000 | 50% cache hit |
| TOTAL (no cache) | | | | | 12,000 | |
| TOTAL (with cache) | | | | | 6,000 | 50% saving |
```

***

### When to Upgrade vs When to Optimize

#### Upgrade Plan if:

* Legitimate high usage (not bugs or inefficiency)
* Usage expected to grow significantly
* Current plan is limiting features (rate limits)
* Cost per credit becomes favorable at higher volumes

**Example: Should You Upgrade?**

```
Current usage: 50,000 credits/month — consistently hitting rate limits.

Options:
1. Optimize usage — reduce waste, implement caching, limit over-fetching
2. Upgrade plan — higher rate limits, more monthly credits included

If hitting rate limits regularly: upgrade for higher req/min limits.
If over-fetching data: optimize first before upgrading.
```

See the [Plans guide](broken://pages/4201be0f307231c3046d46d2ed7a069c7f0275d0) for specific plan details and pricing.

#### Optimize if:

* You have wasteful patterns (duplicates, over-fetching, infrequent changes fetched hourly)
* Current usage seems excessive for your use case
* Easy optimizations available (caching, limiting)

***

### Build vs Buy: Pullbay vs Custom Scrapers

Building and maintaining your own scrapers involves:

* Significant development time upfront
* Ongoing maintenance as source websites change
* Proxy infrastructure and anti-bot handling
* Monitoring for failures and schema changes

Pullbay handles all of this automatically. Your engineering time is spent on using the data, not maintaining the pipeline.

When evaluating total cost, factor in: developer hours for initial build, ongoing maintenance hours per month, infrastructure costs (proxies, servers), and the reliability risk when sites change structure.

***

### FAQ

<details>

<summary>What's the cheapest way to test the API?</summary>

**Use test keys with limited data:**

```python
# Test key (no credits used)
test_client = PullbayClient(api_key=os.getenv("PULLBAY_API_KEY_TEST"))

# Fetch limited sample
sample = test_client.fetch_all(
    "reviews",
    params={
        "business_id": "12345",
        "max_results": 10  # Small sample for testing
    }
)

print(f"Got {len(sample)} reviews, cost: 0 credits")
```

Test keys include:

* 100 requests/hour limit
* 50 items/request soft limit
* Simulated data similar to production
* Zero credit cost

</details>

<details>

<summary>How do I estimate my monthly credit usage?</summary>

**Use the formula:**

```
Monthly Credits = Requests × Pages × Frequency

Examples:
- One business, daily: 1 × 40 × 30 = 1,200 credits
- Ten businesses, hourly: 10 × 5 × 24 × 30 = 36,000 credits
- Five endpoints, daily: (5 × 20 pages × 30) = 3,000 credits
```

Or create a test run:

```python
import time

start_credits = client.get_account()['credits']
start_time = time.time()

# Run your typical workload
perform_typical_usage()

elapsed = time.time() - start_time
credits_used = start_credits - client.get_account()['credits']

daily_requests = requests_made / (elapsed / 86400)
monthly_estimate = (credits_used / requests_made) * (daily_requests * 30)

print(f"Estimated monthly usage: {monthly_estimate:.0f} credits")
```

</details>

<details>

<summary>Can I set a credit spending limit?</summary>

**Most plans support spending limits. Check your dashboard:**

1. Go to **Settings > Billing**
2. Set **Monthly Spending Limit**
3. Receive alerts when approaching limit
4. Automatic cutoff when limit reached

**Programmatic alert instead:**

```python
def check_budget(client, monthly_budget=5000, current_usage=None):
    if current_usage is None:
        current_usage = client.get_usage()['credits_used']

    percent_used = (current_usage / monthly_budget) * 100

    if percent_used >= 100:
        raise Exception("Monthly budget exceeded!")
    elif percent_used >= 75:
        send_alert(f"WARNING: {percent_used:.0f}% of budget used")

    return percent_used

# Usage
percent = check_budget(client, monthly_budget=5000)
print(f"Budget used: {percent:.0f}%")
```

</details>

***

### Summary

**Implement these strategies in order of impact:**

1. **Fetch only what you need** (50-90% savings) - Highest ROI
2. **Cache responses** (30-70% savings) - Quick to implement
3. **Batch related requests** (30-60% savings) - Plan ahead
4. **Avoid duplicates** (20-50% savings) - Refactor for clarity
5. **Schedule smart** (40-80% savings) - Match frequency to data changes
6. **Use test keys** (100% dev savings) - Never use production credits
7. **Monitor and alert** (Prevents waste) - Catch bugs early

**Combined impact:** 50-90% monthly credit reduction while improving performance and code quality.

Start with strategies 1 and 2 for immediate impact. Add the others as your usage scales.


# Reliability Metrics

## Reliability Metrics

Pullbay tracks and publishes reliability metrics for transparency. Understanding these metrics helps you set expectations and design resilient integrations.

***

### Key Metrics

#### Availability

**Definition**\
The percentage of time the service accepts and processes requests.

**How it's measured**\
Availability = (successful checks / total checks) × 100

**What to expect**\
We target **99.9% availability** for production endpoints.

***

#### Success Rate

**Definition**\
The percentage of requests that complete successfully.

**How it's measured**\
(successful requests / total requests) × 100\
Client errors (4xx) are excluded from this calculation.

**Why it matters**\
Success rate reflects both Pullbay’s infrastructure reliability and the stability of upstream data sources.

***

#### Response Time

**Definition**\
The time from when a request is received to when a response is sent.

**Metrics tracked**

| Metric | Description                                                 |
| ------ | ----------------------------------------------------------- |
| p50    | Median response time (50% of requests are faster than this) |
| p95    | 95th percentile (95% of requests are faster than this)      |
| p99    | 99th percentile (only 1% of requests are slower)            |

**Why p95 and p99 matter**\
Average response time can hide slow outliers. p95 and p99 help you understand the performance of slower requests and design better timeouts.

***

### Metric Variations

Metrics can vary depending on several factors:

#### Endpoint Type

* **Standard pagination endpoints** — Faster per request (fetching one page)
* **Managed pagination endpoints** — Slower overall (fetching multiple pages internally)

***

#### Data Source

Different upstream platforms have different reliability profiles. Some are more stable, while others may change more frequently.

***

#### Time of Day

Some data sources may experience higher load at certain times, which can affect response times.

***

### Viewing Metrics

#### Per-endpoint Metrics

Your dashboard provides metrics for each endpoint you use, typically across:

* Last 24 hours
* Last 7 days
* Last 30 days

***

#### Historical Data

Historical metrics help you:

* Identify usage patterns (e.g., weekly cycles)
* Detect performance degradation trends
* Plan capacity and scaling decisions

***

### Setting Expectations

When building integrations, consider the following:

| Factor          | Recommendation                                     |
| --------------- | -------------------------------------------------- |
| Timeout         | Set to at least **2× the p95 response time**       |
| Retry count     | **3 retries** is usually sufficient                |
| Circuit breaker | Trigger after **50% failure rate over 30 seconds** |

***

### What We Don’t Guarantee

#### Data Freshness

Pullbay fetches data from upstream sources. Some data may be cached or delayed.

***

#### Data Completeness

Upstream platforms may limit the amount of data available. Responses may not always include the full dataset.

***

#### Schema Stability

While we aim to keep schemas stable, upstream changes may require updates to response structures.

***

### Summary

Reliability metrics help you understand how Pullbay performs in real-world conditions. By using these metrics to guide your timeout, retry, and error-handling strategies, you can build more stable and resilient integrations.


# Service Health

## Service Health

Pullbay continuously monitors all services and endpoints. This page explains how to check status and what the indicators mean.

***

### Where to Check Status

#### Dashboard

Your Pullbay dashboard shows the current status of all endpoints you use. The overview page displays:

* Overall platform status
* Status of each service
* Recent incidents (if any)

***

#### Status Page

A public status page shows current and historical uptime for all services.

Bookmark it to check during outages.

***

### Status Indicators

Each service and endpoint displays one of the following states:

| Status         | Icon | Meaning                                             |
| -------------- | ---- | --------------------------------------------------- |
| Operational    | 🟢   | Working normally                                    |
| Degraded       | 🟡   | Working but with reduced performance or reliability |
| Partial outage | 🟠   | Some endpoints or regions affected                  |
| Outage         | 🔴   | Service unavailable                                 |

***

### What Each Status Means

#### Operational

Everything is working as expected.\
Request success rates are normal and response times are within expected ranges.

***

#### Degraded

The service is working but you may experience:

* Slower response times
* Occasional timeouts
* Lower success rates than normal

Requests may still succeed, but with higher latency.\
Consider adding extra timeout buffer in your integration.

***

#### Partial Outage

Some parts of the service are affected:

* Specific endpoints might be unavailable while others continue working
* Certain regions may be impacted
* Specific data sources may be temporarily unavailable

Check the incident details to understand which parts are affected.

***

#### Outage

The service is unavailable. Requests will fail.

During outages:

* Implement fallback logic if available
* Queue requests for retry after recovery
* Monitor the status page for updates

***

### Incident Communication

When incidents occur, Pullbay communicates through:

* Status page updates (immediate)
* Dashboard notifications
* Email notifications (for significant incidents)

Incident reports typically include:

* What’s affected
* When it started
* Current status
* Expected resolution time (when known)
* Root cause (in post-incident reports)

***

### Monitoring Your Usage

Beyond service health, you should monitor your own usage:

* **Success rate** — What percentage of your requests succeed
* **Response times** — Whether responses are getting slower
* **Error patterns** — Whether certain endpoints are failing more frequently

Your dashboard provides these metrics broken down by endpoint and time period.

***

### Recommendations

To build resilient integrations:

* Monitor both platform status and your own usage metrics
* Adjust timeouts when performance degrades
* Implement retry logic for transient failures
* Use fallback strategies where possible
* Track error patterns to detect issues early


# Compliance & Responsibility

Pullbay provides access to publicly available data. This page clarifies what that means, your responsibilities, and how accountability is shared.

***

### What “Public Data” Means

Pullbay only accesses data that is:

* Publicly visible on the web
* Accessible without authentication
* Not behind paywalls or login screens

We do not access private data, personal accounts, or protected content.

***

### Shared Responsibility Model

Using Pullbay involves shared responsibility between Pullbay and your organization.

***

#### What Pullbay Handles

| Area            | Responsibility                                      |
| --------------- | --------------------------------------------------- |
| Infrastructure  | Secure, reliable, and available services            |
| Data accuracy   | Fetching data accurately from sources               |
| API security    | Protecting our API and your credentials             |
| Maintenance     | Keeping endpoints working as sources change         |
| Transparency    | Clear documentation and status reporting            |
| Data in transit | Encrypted connections between our systems and yours |

***

#### What You Handle

| Area                | Your Commitment                                  |
| ------------------- | ------------------------------------------------ |
| API key security    | Keeping your keys secret and secure              |
| Legal compliance    | Using data in accordance with applicable laws    |
| Usage monitoring    | Tracking your consumption and costs              |
| Integration quality | Building reliable integrations                   |
| Data handling       | Properly storing and processing data you receive |
| Data at rest        | Securing data within your systems                |

***

### Your Responsibilities

#### Legal Compliance

You are responsible for ensuring your use of Pullbay data complies with:

* Applicable laws in your jurisdiction
* Data protection regulations (e.g., GDPR, CCPA)
* Platform terms of service
* Any relevant industry-specific regulations

***

#### Ethical Use

Use data responsibly:

* Do not use data to harass or target individuals
* Respect the context in which the data was shared
* Consider privacy implications, even for public data

***

#### Data Handling

If you store or process data from Pullbay:

* Implement appropriate security measures
* Handle personal data according to applicable regulations
* Maintain a data retention policy
* Honor data subject rights where applicable

***

#### Account Security

* Use strong passwords
* Enable additional security features where available
* Monitor account activity
* Report suspicious activity

***

### Acceptable Use

You agree not to:

* Use Pullbay for illegal purposes
* Attempt to bypass rate limits or security controls
* Resell raw API access without authorization
* Use data to harm individuals
* Overwhelm the system with excessive requests

***

### Liability

Pullbay provides data **“as is”** from public sources.

We are not liable for:

* Decisions made based on the data
* Business outcomes resulting from data usage
* Third-party actions related to your use
* Compliance with laws specific to your use case

***

### Questions

If you are unsure whether a use case is appropriate, contact us before proceeding. We are available to discuss specific scenarios.


# Pullbay for SaaS Companies

B2B SaaS apps live on app stores just like consumer apps—and your reviews tell a story that your support tickets might miss. Customer success teams, product leaders, and sales teams can use app store reviews to predict churn, identify at-risk accounts, and understand competitive gaps. Pullbay's App Store Reviews API makes it easy for SaaS teams to monitor reviews across your app and competitors' apps, unlocking insights that feed directly into customer success workflows.

## Why SaaS Companies Need App Store Review Monitoring

Unlike consumer apps, B2B SaaS apps have a unique advantage: reviews often come from the same people who signed a contract with you. When a user leaves a 1-star review, they're often flagging a real problem before they cancel their subscription. You can reach out proactively.

Additionally, your app store presence is a sales channel. Prospects research your app by reading reviews. If your competitors have better reviews, you lose deals. If specific reviews mention problems you've already fixed, outdated negative reviews are costing you pipeline.

App store reviews also provide competitive intelligence: if a competitor's reviews are full of complaints about missing features, that's a sales angle. If they're praising a feature you don't have, that's a roadmap signal.

Pullbay lets you:

* **Monitor customer sentiment** across releases and track NPS-like metrics without surveys
* **Flag at-risk accounts** for proactive customer success outreach
* **Conduct competitive intelligence** on competitor apps
* **Pull historical review data** to baseline your performance
* **Automate review monitoring** across multiple apps without manual work

## Setting Up App Store Review Monitoring for SaaS

### Understanding Your Review Landscape

Start by pulling reviews for your app and two key competitors. This baseline helps you understand where you stand:

```python
import requests
from datetime import datetime, timedelta

api_url = "https://api.pullbay.com/appstore/reviews"
headers = {"Authorization": "Bearer YOUR_API_KEY"}

# Pull 3 months of historical data for baseline analysis
apps = {
    "Your App": "com.yourcompany.app",
    "Competitor A": "com.competitor1.app",
    "Competitor B": "com.competitor2.app"
}

for app_name, app_id in apps.items():
    params = {"app_id": app_id, "limit": 1000}
    response = requests.get(api_url, headers=headers, params=params)
    reviews = response.json()["reviews"]

    avg_rating = sum(r["rating"] for r in reviews) / len(reviews)
    total_reviews = len(reviews)

    print(f"{app_name}: {avg_rating:.2f} stars ({total_reviews} reviews)")
```

This baseline tells you: Are you rated higher or lower than competitors? Is your review volume growing or shrinking? Are competitor reviews more positive or negative?

### Monitor for Customer Health Signals

SaaS customer success teams should monitor reviews for churn signals. A user who leaves a 1- or 2-star review is often a churn risk within 30 days. While you can't always identify which company left the review (reviews are anonymous), you can flag patterns:

* **Sudden drop in rating for your app** → Something broke in the latest release; customer satisfaction declined
* **Spike in 1-star reviews mentioning specific features** → Customers value those features; consider whether they're at risk of churn
* **Reviews mentioning "support" negatively** → Customer success team needs to investigate support quality
* **Competitor reviews praising a feature you lack** → Sales and product should track this

### Connecting Reviews to Customer Accounts

SaaS teams often want to match reviews back to customer accounts for proactive outreach. This is manual but high-value:

**Workflow:**

1. Pull 1- and 2-star reviews from your app
2. Look for identifying information in the review text (company name, team size, use case, feature mentioned)
3. Search your CRM for customer accounts matching those signals
4. Flag the account for a customer success check-in
5. Document the review in the account record

Example: A review says "We use this for team scheduling, but the mobile app keeps losing our data." You search your CRM for customers who bought the "team scheduling" feature. You find Acme Corp, check their usage, and discover they churned last month—the review explained why.

## Competitive Intelligence Workflow

Monitor competitor apps to understand your competitive position:

```python
import requests
from collections import Counter

api_url = "https://api.pullbay.com/appstore/reviews"
headers = {"Authorization": "Bearer YOUR_API_KEY"}

def analyze_competitor_reviews(app_id, feature_keywords):
    """
    Fetch competitor reviews and extract feature mentions.
    Returns top features mentioned in their reviews.
    """
    params = {"app_id": app_id, "limit": 500}
    response = requests.get(api_url, headers=headers, params=params)
    reviews = response.json()["reviews"]

    feature_mentions = Counter()

    for review in reviews:
        review_text = review["content"].lower()
        for feature, keywords in feature_keywords.items():
            if any(keyword in review_text for keyword in keywords):
                feature_mentions[feature] += 1

    return dict(feature_mentions.most_common(10))

# Define features you're tracking
tracked_features = {
    "automation": ["automate", "workflow", "scheduled", "recurring"],
    "reporting": ["report", "export", "analytics", "dashboard"],
    "mobile": ["mobile", "iphone", "android", "app"],
    "integrations": ["slack", "salesforce", "jira", "integrate"],
}

competitor_reviews = analyze_competitor_reviews("com.competitor1.app", tracked_features)
print("Top competitor feature mentions:", competitor_reviews)
```

This tells you what competitors' customers are asking for and praising. If their reviews praise integrations but yours mention missing integrations, that's a gap worth filling.

## Measuring SaaS Success Metrics from Reviews

Track these metrics over time:

| Metric                          | Formula                                 | Signal                                                 |
| ------------------------------- | --------------------------------------- | ------------------------------------------------------ |
| **Average Rating**              | Sum of all ratings / count of reviews   | Overall satisfaction; track week-over-week             |
| **5-Star Percentage**           | (5-star reviews / total reviews) \* 100 | Customer delight; target 60%+ for healthy SaaS         |
| **Negative Review Rate**        | (1- and 2-star / total reviews) \* 100  | Churn risk indicator; rising rate = investigate        |
| **Review Velocity**             | Reviews per day or per week             | Growth metric; declining velocity = engagement drop    |
| **Sentiment Change by Version** | Avg rating for v2.5 vs. v2.4            | Release quality indicator; negative trend = regression |

## Example: Competitive Intelligence in Action

Imagine you sell project management software. You pull reviews for your app and your three main competitors monthly:

**Month 1:**

* Your app: 4.2 stars, 500 reviews
* Competitor A: 3.8 stars, 800 reviews
* Competitor B: 4.0 stars, 600 reviews

You're winning on rating. But when you analyze what Competitor A's 1- and 2-star reviews mention:

* 45 reviews mention "mobile app is slow"
* 38 reviews mention "no offline support"
* 25 reviews mention "poor integrations"

You have a fast mobile app and offline support—that's a sales angle. Your sales team immediately updates positioning: "Unlike Competitor A, we offer offline support and a fast mobile experience."

Meanwhile, your 1- and 2-star reviews are dominated by "missing API" and "no custom reports." You add these to your roadmap and monitor whether competitive pressure increases.

## Scale Monitoring: Multi-App Management

If you operate multiple SaaS products or white-label apps for clients, Pullbay's credit system scales efficiently:

* **Free plan**: 10 requests/minute (great for monitoring 1–2 apps)
* **Starter plan**: 60 requests/minute (monitor 5–10 apps with weekly reports)
* **Growth plan**: 300 requests/minute (monitor 20+ apps with daily updates)
* **Scale plan**: 1000+ requests/minute (enterprise-scale multi-app monitoring)

Credits are consumed only on successful requests, so you're never paying for failed API calls.

## Automating Review Monitoring

Set up a weekly cron job to automatically pull reviews and generate reports:

```python
import requests
import json
from datetime import datetime

def generate_weekly_report(app_id, app_name):
    """Generate a weekly review health report."""
    api_url = "https://api.pullbay.com/appstore/reviews"
    headers = {"Authorization": "Bearer YOUR_API_KEY"}

    params = {"app_id": app_id, "limit": 200}
    response = requests.get(api_url, headers=headers, params=params)
    reviews = response.json()["reviews"]

    ratings = [r["rating"] for r in reviews]
    avg_rating = sum(ratings) / len(ratings)

    report = {
        "app": app_name,
        "week": datetime.now().strftime("%Y-W%W"),
        "avg_rating": round(avg_rating, 2),
        "total_reviews": len(reviews),
        "5_star_count": len([r for r in reviews if r["rating"] == 5]),
        "1_star_count": len([r for r in reviews if r["rating"] == 1]),
    }

    return report

# Generate reports for your apps
apps = {"Your App": "com.yourcompany.app", "Client App": "com.client.app"}
for app_name, app_id in apps.items():
    report = generate_weekly_report(app_id, app_name)
    print(json.dumps(report, indent=2))
```

Store these reports in a shared dashboard or send them to Slack weekly. Your team stays informed without manual work.

## Get Started with SaaS Review Monitoring

1. **Sign up for free**: Create a Pullbay account and get your API key
2. **Pull historical data**: Use `/appstore/reviews` to fetch review data for baseline analysis
3. **Set up monitoring**: Configure weekly or daily pulls for your app and 2–3 competitors
4. **Integrate with customer success**: Train your CS team to review flagged accounts monthly
5. **Track metrics**: Create a simple dashboard to track rating trends, review volume, and competitive positioning
6. **Iterate**: Use review insights to prioritize roadmap, respond to competitive threats, and proactively reach out to at-risk customers

App store reviews are a goldmine for SaaS teams. Pullbay makes it simple to mine that data, automate your workflow, and turn reviews into customer success actions.

***

*Ready to leverage app store reviews for customer success? Get your free API key at Pullbay and start monitoring reviews today.*


# Pullbay for Product Managers

Your app store reviews are a goldmine of user feedback, but they're scattered across platforms and buried under noise. As a product manager, you know that user feedback is your competitive edge—but manually scrolling through hundreds of reviews each week isn't scalable. Pullbay's App Store Reviews API lets you programmatically fetch, analyze, and act on user feedback in minutes instead of weeks.

## The Product Manager's Problem

Product teams face a critical gap: critical user feedback lives on the app store, but it's disconnected from your product roadmap. Users report bugs in 2-star reviews, suggest features in 5-star reviews, and reveal churn risk in scathing comments—but without a systematic way to capture this data, it's invisible to engineering and product leadership.

When a spike in 1-star reviews mentions "crashes on login," you might not know until customer support is overwhelmed. When 5-star reviews celebrate a feature you haven't shipped yet, you miss validation of your roadmap. When recurring complaints appear across dozens of reviews, the signal gets lost in the noise.

Pullbay solves this: pull structured review data directly into your systems, automate the extraction of actionable signals, and feed insights directly into your product tools (Jira, Linear, Notion, Slack).

## How Pullbay Helps Product Managers

### Bulk-Fetch and Organize Reviews

Instead of browsing the app store, make a single API request to pull all reviews for your app. Each review includes the app ID, rating (1–5 stars), review text, release version, and timestamp. Store them in your database or pass them to analysis tools.

### Identify Feature Requests Before They're Scheduled

Filter for 4- and 5-star reviews mentioning specific keywords ("dark mode," "offline sync," "export CSV"). Track how many times a feature request appears across reviews. When 30 five-star reviews in a month mention the same missing feature, that's validation signal.

### Detect Bugs Your Engineering Team Hasn't Seen Yet

Spike detection: when 1-star reviews suddenly jump 150% in a day, pull them immediately. Search the text for keywords like "crash," "frozen," "won't load," or "data lost." Match reviews to release versions to pinpoint which update broke things. Escalate to engineering with specific user complaints attached.

### Track Sentiment Across Releases

Pull reviews for your current version, previous version, and a competitor's version. Compare average rating and sentiment over time. Did your last release improve perception? Are users happier than competitors' users?

## A Practical Workflow: From Reviews to Product Action

{% stepper %}
{% step %}

### Weekly Review Fetch

```python
import requests
import json
from datetime import datetime, timedelta

# Fetch all reviews from the past 7 days
api_url = "https://api.pullbay.com/appstore/reviews"
headers = {"Authorization": "Bearer YOUR_API_KEY"}

params = {
    "app_id": "your_app_id",  # e.g., "com.myapp"
    "limit": 500
}

response = requests.get(api_url, headers=headers, params=params)
reviews = response.json()["reviews"]

print(f"Fetched {len(reviews)} reviews this week")
```

{% endstep %}

{% step %}

### Filter and Categorize

Product managers group reviews by signal:

* **1–2 star reviews** → Bugs and critical issues
* **3 star reviews** → Feature requests and friction points
* **5 star reviews** → What's working; testimonial candidates
  {% endstep %}

{% step %}

### Extract Recurring Themes

Manually review the 1–2 star reviews and look for repeated complaints. Example:

* "App crashes on Android 13" (appears 8 times in 2 weeks)
* "Can't export reports as PDF" (appears 12 times in 3 weeks)
* "Login takes 30 seconds" (appears 5 times in 2 weeks)
  {% endstep %}

{% step %}

### Convert to Product Action

Create a ticket in Jira/Linear for each theme, with the user feedback attached. Include the count of reviews mentioning the problem. Link to the specific reviews in your Pullbay dashboard or reports.
{% endstep %}
{% endstepper %}

## Review Signals → Product Actions Reference

Use this table to decide what action each review pattern warrants:

| Signal                    | Evidence                                                                 | Action                                                   |
| ------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------- |
| **P0 Bug**                | Spike in 1-star + keywords: "crash," "won't load," "data lost"           | Escalate to engineering; create blocker ticket           |
| **Feature Validation**    | Spike in 5-star + same feature request (10+ mentions/week)               | Add to roadmap; validate with customer interviews        |
| **Churn Risk**            | Spike in 2-star + complaints about price, support, competitor comparison | Notify customer success; flag at-risk accounts           |
| **Competitor Gap**        | Competitor app reviews mention a feature you lack (appears 20+ times)    | Evaluate for roadmap; design research sprint             |
| **Messaging Opportunity** | 5-star reviews spontaneously mention a benefit you didn't advertise      | Highlight in marketing; validate product positioning     |
| **Release Impact**        | Average rating drops >0.3 points after a release                         | Investigate changelog; prioritize fixes for next release |

## Practical Example: Detecting a Release Regression

Your team ships v2.5 on Monday. By Wednesday, you run your weekly review fetch:

```json
{
  "app_id": "com.myapp",
  "version": "2.5",
  "rating_distribution": {
    "5_star": 45,
    "4_star": 30,
    "3_star": 15,
    "2_star": 25,
    "1_star": 85
  },
  "trend_vs_v2.4": "-0.6 stars average"
}
```

The 1-star count jumped from \~30/week to 85/week. You pull the newest 1-star reviews:

> "2.5 broke everything. Crashes whenever I open settings." "Update made the app unusable. Keeps crashing on Android." "Don't update to 2.5. Major crashes."

You immediately create a P0 ticket, link the reviews, and escalate to engineering with specific reproduction steps from user feedback. Engineering can fix and release a hotfix by Friday—all because you caught the signal 48 hours earlier than support tickets would have surfaced it.

## Why Product Managers Choose Pullbay

* **Speed**: Fetch reviews in seconds instead of manually scrolling
* **Scale**: Handle hundreds or thousands of reviews without manual triage
* **Integration**: Pipe review data into Jira, Linear, Slack, or your data warehouse
* **Competitive Intelligence**: Compare your reviews against competitors' apps
* **Historical Analysis**: Use `/appstore/reviews` to pull review data for trend analysis

## Get Started

1. **Sign up for free**: Create a Pullbay account and get your API key
2. **Make your first request**: Pull reviews for your app using the code snippet above
3. **Set up weekly automation**: Schedule a cron job or Zapier workflow to fetch reviews every Monday
4. **Create a dashboard**: Store reviews in a spreadsheet or BI tool; track rating trends and volume
5. **Iterate**: Assign team members to triage reviews, create tickets, and track product impact

Start turning scattered user feedback into a structured product signal. Your next product breakthrough is probably already in your app store reviews.

***

*Ready to stop missing critical user feedback? Get your free API key at Pullbay and start monitoring app store reviews today.*


# Pullbay for Mobile Agencies

Mobile agencies manage dozens—sometimes hundreds—of client apps across their portfolio. Keeping track of how each app is performing in the app store is a manual nightmare: scrolling through app store pages, manually counting reviews, manually checking ratings, manually compiling reports for each client. Pullbay's App Store Reviews API lets agencies automate this entire workflow, deliver professional review health reports to clients, and scale monitoring across their entire portfolio without per-seat costs or manual busywork.

## The Agency Challenge: Scale Without Overhead

A typical mobile agency faces this scenario:

* You manage 30 client apps across iOS and Android
* Each client wants a monthly report on app store health
* Each client is asking different questions: "Are we getting good reviews?" "Is our rating improving?" "How do we compare to competitors?"
* Your team is spending 10–15 hours per month manually pulling data and compiling reports

Pullbay solves this by letting you fetch review data for all 30 apps with a few API calls, then generate consistent, professional reports for each client.

## Why Agencies Need Review Monitoring

### Tracking Client App Health

App store reviews are a key health metric for your clients' apps, just like active users or retention. When you publish an app or release a major update, the first signal of success (or failure) is the app store review rating. A sudden drop in rating after a release means something broke. A spike in negative reviews means customer success is declining.

**Early warning system for problems:**

* Release ships on Monday
* By Wednesday, you pull reviews and see average rating dropped 0.5 stars
* You immediately alert the client: "We need to investigate"
* Client fixes the issue and ships a hotfix
* Rating recovers by Friday
* Client prevents a major churn event

Without automated monitoring, you might not catch this until your client calls you upset about poor reviews.

### Competitive Intelligence for Clients

Clients often want to understand how they stack up against competitors. "Is our app rated higher or lower than our competitor?" "Do their reviews mention features we lack?" Pullbay lets you pull competitor data and give clients monthly competitive benchmarks.

### Client Reporting and Retention

Clients love data. A monthly review health report (rating trend, review volume, competitive benchmark, top themes) demonstrates value and keeps clients engaged. It also gives you a reason to have monthly check-in calls—"Let's review your app's performance this month."

## Setting Up Multi-App Monitoring for Agencies

### Organizing Client Apps

Start by storing your client app IDs in a structured format. This makes it easy to scale:

```python
# clients.json - Store this in your system
clients = {
    "Acme Corp": {
        "app_id": "com.acme.app",
        "ios_id": "1234567890",
        "competitors": ["com.competitor1.app", "com.competitor2.app"],
        "contact_email": "pm@acme.com"
    },
    "Widget Inc": {
        "app_id": "com.widget.app",
        "ios_id": "0987654321",
        "competitors": ["com.competitor3.app"],
        "contact_email": "product@widget.com"
    },
    # ... 28 more clients
}
```

### Fetching Reviews for All Clients

Write a simple script that loops through all clients and fetches their reviews:

```python
import requests
import json
from datetime import datetime

api_url = "https://api.pullbay.com/appstore/reviews"
headers = {"Authorization": "Bearer YOUR_API_KEY"}

def fetch_client_reviews(app_id, limit=200):
    """Fetch reviews for a single app."""
    params = {"app_id": app_id, "limit": limit}
    response = requests.get(api_url, headers=headers, params=params)

    if response.status_code == 200:
        return response.json()["reviews"]
    else:
        print(f"Error fetching reviews for {app_id}: {response.status_code}")
        return []

def generate_client_summary(client_name, app_id, reviews):
    """Generate a summary for a single client app."""
    if not reviews:
        return None

    ratings = [r["rating"] for r in reviews]
    avg_rating = sum(ratings) / len(ratings)

    summary = {
        "client": client_name,
        "app_id": app_id,
        "timestamp": datetime.now().isoformat(),
        "metrics": {
            "avg_rating": round(avg_rating, 2),
            "total_reviews": len(reviews),
            "5_star_count": len([r for r in reviews if r["rating"] == 5]),
            "4_star_count": len([r for r in reviews if r["rating"] == 4]),
            "3_star_count": len([r for r in reviews if r["rating"] == 3]),
            "2_star_count": len([r for r in reviews if r["rating"] == 2]),
            "1_star_count": len([r for r in reviews if r["rating"] == 1]),
            "5_star_percentage": round(len([r for r in reviews if r["rating"] == 5]) / len(reviews) * 100, 1),
        }
    }

    return summary

# Load clients and generate summaries
clients = {
    "Acme Corp": "com.acme.app",
    "Widget Inc": "com.widget.app",
    # ... all other clients
}

all_summaries = []

for client_name, app_id in clients.items():
    reviews = fetch_client_reviews(app_id)
    summary = generate_client_summary(client_name, app_id, reviews)

    if summary:
        all_summaries.append(summary)
        print(f"✓ {client_name}: {summary['metrics']['avg_rating']} stars ({summary['metrics']['total_reviews']} reviews)")

# Save summaries to a JSON file or database
with open("client_summaries.json", "w") as f:
    json.dump(all_summaries, f, indent=2)

print(f"\nGenerated summaries for {len(all_summaries)} clients")
```

This script fetches reviews for all your clients in one run, consumes credits efficiently, and generates consistent metrics for each client.

## Generating Weekly Client Digests

Create a professional weekly or monthly report for each client. Here's a template:

```python
def generate_client_report(client_name, app_id, competitors_dict=None):
    """
    Generate a comprehensive weekly report for a client.
    """
    api_url = "https://api.pullbay.com/appstore/reviews"
    headers = {"Authorization": "Bearer YOUR_API_KEY"}

    # Fetch client app reviews
    params = {"app_id": app_id, "limit": 300}
    response = requests.get(api_url, headers=headers, params=params)
    client_reviews = response.json()["reviews"]

    client_ratings = [r["rating"] for r in client_reviews]
    client_avg_rating = sum(client_ratings) / len(client_ratings)

    report = {
        "client": client_name,
        "week": datetime.now().strftime("%Y-W%W"),
        "app_health": {
            "avg_rating": round(client_avg_rating, 2),
            "total_reviews": len(client_reviews),
            "5_star_percentage": round(len([r for r in client_reviews if r["rating"] == 5]) / len(client_reviews) * 100, 1),
            "1_star_count": len([r for r in client_reviews if r["rating"] == 1]),
            "trend": "improving" if client_avg_rating > 4.0 else "stable" if client_avg_rating > 3.7 else "declining"
        },
        "top_5_star_themes": extract_themes(client_reviews, rating=5),
        "top_1_star_themes": extract_themes(client_reviews, rating=1),
    }

    # Add competitive comparison if available
    if competitors_dict:
        report["competitive_analysis"] = {}
        for comp_name, comp_id in competitors_dict.items():
            comp_response = requests.get(api_url, headers=headers, params={"app_id": comp_id, "limit": 300})
            comp_reviews = comp_response.json()["reviews"]
            comp_ratings = [r["rating"] for r in comp_reviews]
            comp_avg = sum(comp_ratings) / len(comp_ratings)

            report["competitive_analysis"][comp_name] = {
                "avg_rating": round(comp_avg, 2),
                "vs_client": round(client_avg_rating - comp_avg, 2)
            }

    return report

# Generate report for one client
report = generate_client_report(
    "Acme Corp",
    "com.acme.app",
    competitors_dict={"Competitor A": "com.competitor1.app"}
)

print(json.dumps(report, indent=2))
```

**Sample report output:**

```json
{
  "client": "Acme Corp",
  "week": "2026-W15",
  "app_health": {
    "avg_rating": 4.3,
    "total_reviews": 156,
    "5_star_percentage": 62.8,
    "1_star_count": 8,
    "trend": "improving"
  },
  "competitive_analysis": {
    "Competitor A": {
      "avg_rating": 3.9,
      "vs_client": 0.4
    }
  }
}
```

## Identifying Review Themes Automatically

Extract top themes from 1-star and 5-star reviews to identify what's working and what's broken:

```python
def extract_themes(reviews, rating=5, top_keywords=None):
    """
    Extract common themes from reviews with a specific rating.
    Returns most-mentioned keywords.
    """
    if top_keywords is None:
        top_keywords = {
            "performance": ["slow", "lag", "fast", "smooth", "crash", "freeze"],
            "usability": ["easy", "intuitive", "complicated", "confusing", "hard"],
            "features": ["feature", "missing", "request", "support", "doesn't"],
            "support": ["support", "help", "response", "customer service"],
            "price": ["price", "expensive", "cost", "affordable", "value"],
            "reliability": ["reliable", "stable", "bugs", "glitch", "broken"],
        }

    matching_reviews = [r for r in reviews if r["rating"] == rating]
    text = " ".join([r["content"].lower() for r in matching_reviews])

    theme_counts = {}
    for theme, keywords in top_keywords.items():
        count = sum(text.count(keyword) for keyword in keywords)
        if count > 0:
            theme_counts[theme] = count

    return dict(sorted(theme_counts.items(), key=lambda x: x[1], reverse=True))

# Example usage
five_star_themes = extract_themes(reviews, rating=5)
one_star_themes = extract_themes(reviews, rating=1)

print("Top themes in 5-star reviews:", five_star_themes)
print("Top themes in 1-star reviews:", one_star_themes)
```

Output:

```
Top themes in 5-star reviews: {'features': 15, 'ease': 12, 'performance': 8}
Top themes in 1-star reviews: {'performance': 22, 'features': 8, 'support': 5}
```

Your client can immediately see: "Users love our features and ease of use, but performance is our biggest pain point."

## Automating Weekly Digests to Clients

Schedule your monitoring script to run weekly, generate reports for all clients, and email each client their report:

```python
import json
from datetime import datetime

def send_client_report_email(client_email, report_data):
    """
    Format and send a client report via email.
    (Uses your preferred email service: SendGrid, AWS SES, etc.)
    """
    subject = f"Weekly App Store Health Report: {report_data['client']}"

    body = f"""
Hi {report_data['client']},

Here's your app store health report for {report_data['week']}:

OVERALL HEALTH
- Average Rating: {report_data['app_health']['avg_rating']} stars
- Total Reviews: {report_data['app_health']['total_reviews']}
- 5-Star Reviews: {report_data['app_health']['5_star_percentage']}%
- Trend: {report_data['app_health']['trend'].upper()}

TOP THEMES (5-STAR REVIEWS)
{format_themes(report_data['top_5_star_themes'])}

TOP THEMES (1-STAR REVIEWS)
{format_themes(report_data['top_1_star_themes'])}

COMPETITIVE COMPARISON
{format_competitive(report_data.get('competitive_analysis', {}))}

Questions? Reply to this email or schedule a call with your account manager.

Best regards,
Your Agency
    """

    # Send via your email service
    # send_email(to=client_email, subject=subject, body=body)

def format_themes(themes):
    return "\n".join([f"- {theme.title()}: {count} mentions" for theme, count in themes.items()])

def format_competitive(competitive_data):
    if not competitive_data:
        return "Not available this week"
    return "\n".join([f"- {comp}: {metrics['vs_client']:+.1f} vs your rating" for comp, metrics in competitive_data.items()])
```

Each client gets a professional, data-driven report every week. They see their rating trend, what customers love, what they hate, and how they compare to competitors. This adds enormous value and keeps clients engaged.

## Scaling Across Your Portfolio

**Rate limits and credit efficiency:**

* **Free plan** (10 req/min): Works for 1–2 client apps
* **Starter plan** (60 req/min): Monitor 5–10 apps with weekly reports
* **Growth plan** (300 req/min): Monitor 20+ apps with weekly updates
* **Scale plan** (1000+ req/min): Monitor 50+ apps with daily monitoring

Credits are consumed only on successful requests, so you're never paying for failed API calls. No per-seat pricing—whether you have 5 clients or 500, you pay only for the API calls.

**Example: Scaling to 30 clients**

With a Growth plan (300 req/min), you can:

* Pull reviews for 30 client apps: 30 requests
* Pull reviews for 60 competitor apps (2 per client): 60 requests
* Extract themes and generate summaries: 10 requests
* Total: 100 requests per week to monitor 30 clients

That's less than 0.5% of your rate limit. You could run this weekly with room to spare.

## Example Workflow: Complete Monthly Report for a Client

Here's what a real weekly workflow looks like:

{% stepper %}
{% step %}

### Monday morning (automated)

1. Script runs and fetches reviews for all 30 client apps + competitors
2. Generates summary metrics and extracts themes for each client
3. Creates JSON reports and stores them in a database or S3 bucket
   {% endstep %}

{% step %}

### Tuesday morning

1. You review the week's data and flag any anomalies
2. You write brief narrative summaries for each client report
3. System sends personalized emails to each client
   {% endstep %}

{% step %}

### Client receives their report

* Acme Corp sees their rating is 4.3 stars, up 0.1 from last week
* They see that "performance" is mentioned in 22 one-star reviews
* They see they're rated 0.4 stars higher than their nearest competitor
* They understand exactly what they need to focus on
  {% endstep %}

{% step %}

### Client calls you

* "Our performance issues are costing us reviews—we need to prioritize that bug fix"
* "We're beating competitors—let's use that in our marketing"
* You're now a strategic partner, not just a developer shop
  {% endstep %}
  {% endstepper %}

## Get Started: Multi-App Monitoring for Agencies

1. **Sign up for Starter or Growth plan**: Pullbay pricing scales with your portfolio
2. **Store client app IDs**: Create a JSON file or spreadsheet with all your clients' app IDs and competitors
3. **Write your monitoring script**: Use the Python examples above as a template
4. **Schedule weekly runs**: Set up a cron job to run your script every Monday morning
5. **Create an email template**: Generate professional client reports and send them automatically
6. **Review and iterate**: Look for anomalies in the data; use insights to improve client apps
7. **Build a dashboard**: Store reports in a database and create an agency dashboard showing all clients' app health

Pullbay turns manual, time-consuming app store monitoring into a scalable, automated workflow. Your clients get professional reports, you get valuable competitive intelligence, and your team saves 10+ hours per month.

***

*Ready to scale your agency's app monitoring? Get your free API key at Pullbay and start generating client reports today.*


# Pullbay for Marketing Teams

Your best marketing asset might already exist in your app store reviews. Thousands of satisfied customers have left detailed testimonials—praise for specific features, explanations of how they use your app, and social proof that drives conversions. Yet most marketing teams never tap this goldmine. Pullbay's App Store Reviews API makes it easy to extract, organize, and use app store reviews in your marketing campaigns, from landing pages to case studies to social proof widgets.

## The Marketing Case for App Store Reviews

App store reviews serve as powerful social proof. When prospects visit your app store page and see hundreds of 5-star reviews praising your product, they're significantly more likely to try your app. Reviews also tell you what messaging resonates with users—which features they spontaneously praise, which pain points they highlight, and which value propositions stick.

Marketing teams can use review data to:

* **Mine testimonials**: Extract 5-star reviews and use them as social proof on your website
* **Understand messaging that resonates**: Track which features or benefits users mention most in positive reviews
* **Measure campaign impact**: Correlate spikes in positive reviews with your marketing campaigns
* **Monitor brand sentiment**: Track average rating and review volume as KPIs for brand health
* **Identify use cases**: Discover how different customer segments use your product by analyzing review text
* **Benchmark against competitors**: Understand whether your reviews are better or worse than competitors'

## Mining Testimonials from App Store Reviews

The best testimonials are authentic, specific, and short enough to quote on a landing page. Here's how to find them:

```python
import requests

api_url = "https://api.pullbay.com/appstore/reviews"
headers = {"Authorization": "Bearer YOUR_API_KEY"}

def extract_testimonials(app_id, min_length=100):
    """
    Extract high-quality testimonials from 5-star reviews.
    Returns reviews with rating==5 and content length > 100 characters.
    """
    params = {"app_id": app_id, "limit": 500}
    response = requests.get(api_url, headers=headers, params=params)
    reviews = response.json()["reviews"]

    testimonials = [
        {
            "rating": r["rating"],
            "text": r["content"],
            "version": r["version"],
            "date": r["date"]
        }
        for r in reviews
        if r["rating"] == 5 and len(r["content"]) > min_length
    ]

    return testimonials

# Extract testimonials for your app
testimonials = extract_testimonials("com.yourcompany.app")

print(f"Found {len(testimonials)} high-quality testimonials")
for t in testimonials[:5]:
    print(f"\n⭐ {t['text'][:150]}...")
    print(f"   Version: {t['version']} | Date: {t['date']}")
```

You now have a database of authentic customer quotes. Use these on:

* Landing pages ("What Users Say")
* Case study pages
* Sales collateral
* Social media posts
* App store listing optimization
* Email campaigns

## Identifying Messaging That Resonates

Track which features or benefits customers mention most in 5-star reviews. This tells you what messaging actually resonates—not what marketing thinks resonates, but what customers care about.

**Workflow:**

1. Pull 5-star reviews monthly
2. Read through them and manually identify recurring themes
3. Count mentions of specific features, benefits, or pain points
4. Update your messaging to emphasize what customers actually value

Example themes from 5-star reviews:

* 47 reviews mention "saves time" or "efficient"
* 32 reviews mention "easy to use" or "intuitive"
* 28 reviews mention "reliable" or "never crashes"
* 19 reviews mention "customer support" positively
* 15 reviews mention "affordable" or "good value"

Your marketing messaging should emphasize time savings, ease of use, and reliability—because that's what customers are already praising.

Competitor comparison: If competitor apps have high review counts but reviews focus on "works, but buggy" while your reviews emphasize "smooth, reliable, and beautiful"—that's positioning gold.

## Tracking Campaign Impact with Review Velocity

When you launch a marketing campaign (product launch, rebrand, big feature release), monitor whether review volume and rating increase afterward.

**Before campaign:**

* Week 1: 12 reviews/day, 4.2 avg rating
* Week 2: 10 reviews/day, 4.1 avg rating

**Launch week (ad spend increased, new landing page):**

* Week 3: 28 reviews/day, 4.5 avg rating
* Week 4: 35 reviews/day, 4.6 avg rating

The spike in reviews and uptick in rating suggests the campaign worked. More importantly, the reviews themselves contain clues about why—read the new reviews to see what messaging connected.

Conversely, a campaign that didn't move review metrics probably didn't resonate with users.

## Measuring Brand Health: Review-Based KPIs

Track these metrics like you'd track any other marketing KPI:

| Metric                    | Why It Matters                                                   | Target                       |
| ------------------------- | ---------------------------------------------------------------- | ---------------------------- |
| **Average Rating**        | Higher rating = more conversions on app store page               | 4.3+ stars                   |
| **5-Star Percentage**     | Proportion of "delight" vs. satisfaction                         | 55%+                         |
| **Review Velocity**       | Reviews per week; growth signal                                  | Trending up month-over-month |
| **Positive Mention Rate** | % of reviews mentioning specific feature/benefit                 | Track trending keywords      |
| **Sentiment Stability**   | Does rating stay consistent? Spikes indicate issues or successes | Stable ±0.2 points           |
| **Competitive Gap**       | (Your rating – competitor average rating)                        | Positive gap = advantage     |

Create a simple dashboard showing these metrics. Share it with leadership monthly. When review metrics decline, that's a signal for the marketing or product team to investigate.

## Understanding Customer Segments from Reviews

Review text reveals how different customers use your product. Extract themes by customer segment:

* **Enterprise customers** (larger companies) might emphasize "security," "integrations," "reporting"
* **Small businesses** might praise "affordable," "simple," "no learning curve"
* **Technical users** might mention "API," "customizable," "flexible"
* **Non-technical users** might praise "intuitive," "guided setup," "support"

When you see a cluster of reviews from a segment you didn't target, that's an untapped opportunity. If small businesses are suddenly adopting your "enterprise" product, that's market intelligence worth acting on.

## Competitive Benchmarking Through Reviews

Pull reviews for your app and your top 2–3 competitors. Compare:

```python
import requests

api_url = "https://api.pullbay.com/appstore/reviews"
headers = {"Authorization": "Bearer YOUR_API_KEY"}

def benchmark_against_competitors(apps_dict):
    """Compare rating and review volume across multiple apps."""
    results = {}

    for app_name, app_id in apps_dict.items():
        params = {"app_id": app_id, "limit": 1000}
        response = requests.get(api_url, headers=headers, params=params)
        reviews = response.json()["reviews"]

        ratings = [r["rating"] for r in reviews]
        avg_rating = sum(ratings) / len(ratings)

        results[app_name] = {
            "avg_rating": round(avg_rating, 2),
            "total_reviews": len(reviews),
            "5_star_pct": round(len([r for r in reviews if r["rating"] == 5]) / len(reviews) * 100, 1)
        }

    return results

# Benchmark
apps = {
    "Your App": "com.yourcompany.app",
    "Competitor A": "com.competitor1.app",
    "Competitor B": "com.competitor2.app"
}

benchmark = benchmark_against_competitors(apps)
for app, metrics in benchmark.items():
    print(f"{app}: {metrics['avg_rating']} stars | {metrics['5_star_pct']}% five-star | {metrics['total_reviews']} reviews")
```

**Competitive positioning:**

* If you're at 4.3 stars and competitors are at 3.9, you have a quality advantage—use it in sales messaging
* If you have fewer reviews but higher rating, emphasize quality over volume in messaging
* If competitors have more reviews, focus on growth initiatives to increase your review volume

## Practical Example: Mining Testimonials for a Landing Page

Your marketing team decides to redesign the "What Users Say" section of your landing page. Here's the workflow:

1. **Extract testimonials** (use the code snippet above)
2. **Filter for diversity**: Include reviews from different user types, use cases, and features
3. **Select the best 4–6** that are most compelling and quote-worthy
4. **Anonymize or get permission** (note: reviews are user-generated and public, so you can use them, but consider asking for user attribution)
5. **Organize by theme**: Group testimonials by use case or feature
6. **A/B test**: Test a page with 4 testimonials vs. 8 vs. none

Example testimonials you might extract:

> "Finally an app that actually saves me hours every week. Best purchase ever." — 5 stars

> "The support team is incredibly helpful and the app just works. Highly recommend." — 5 stars

> "This replaced three other tools I was paying for. Worth every penny." — 5 stars

These are real, authentic customer voices—far more powerful than marketing copy.

## Automating Review Monitoring for Marketing

Set up a weekly digest that lands in your Slack or email. Include:

* This week's average rating
* New testimonial candidates (top 5 by length and helpfulness)
* Review velocity (reviews/day trend)
* Top keywords mentioned (features, benefits, pain points)
* Competitor comparison

This keeps your marketing team informed and generates ideas for campaigns, messaging, and case studies.

## Get Started: Using App Store Reviews in Marketing

1. **Sign up for free**: Create a Pullbay account and get your API key
2. **Extract testimonials**: Pull 5-star reviews and identify the best ones for your website
3. **Analyze messaging**: Look for recurring themes in positive reviews; align your messaging
4. **Track metrics**: Set up a dashboard with average rating, review volume, and 5-star percentage
5. **Mine insights**: Use review data to understand which messaging resonates and which customer segments you're winning
6. **Benchmark competitors**: Compare your reviews against competitors and identify positioning opportunities
7. **Iterate**: Update landing pages, ad copy, and messaging based on what customers are actually saying

App store reviews are your customers speaking directly to prospects. Pullbay makes it simple to listen, extract, and amplify those authentic voices across your marketing.

***

*Ready to turn app store reviews into marketing gold? Get your free API key at Pullbay and start mining testimonials today.*


# Troubleshooting

Quick solutions to the most common Pullbay API problems. Organised by symptom with step-by-step fixes, code examples, and guidance on when to contact support.

***

## Authentication Issues

### Error: `401 Unauthorized`

The API is rejecting your credentials. Work through these checks in order.

**1. Verify the API key exists and is active**

* Go to the Pullbay dashboard → Account Settings → API Keys
* Confirm the key is listed and has not been revoked
* If you recently rotated keys, make sure every integration is using the new key

**2. Check the Authorization header format**

```python
# Correct
headers = {"Authorization": "Bearer YOUR_API_KEY"}

# Common mistakes
headers = {"Authorization": "YOUR_API_KEY"}              # Missing "Bearer "
headers = {"Authorization": "Bearer"}                    # Missing the key itself
headers = {"Api-Key": "YOUR_API_KEY"}                    # Wrong header name
headers = {"X-API-Key": "YOUR_API_KEY"}                  # Wrong header name
headers = {"Authorization": f"bearer {api_key}"}         # Lowercase "bearer" — must be "Bearer"
```

The header name must be `Authorization` (not `X-API-Key`, not `Api-Key`) and the value must start with `Bearer` followed by your key.

**3. Test with curl**

```bash
curl -X GET "https://api.pullbay.com/appstore/reviews?appId=389801252&maxItems=5" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

* `200 OK` with data → key is valid
* `400 Bad Request` with `VALIDATION_ERROR` → key is valid but a parameter is wrong
* `401 Unauthorized` → key itself is invalid or missing

***

### Error: "API key not working after rotation"

You have revoked the old key but not updated every place it was used.

**Locations to check:**

* [ ] Application code (main codebase)
* [ ] `.env` files and config files
* [ ] CI/CD pipelines (GitHub Actions, GitLab CI, Jenkins)
* [ ] Docker images — rebuild and redeploy
* [ ] Serverless functions (AWS Lambda, Google Cloud Functions)
* [ ] External automation tools (n8n, Zapier, Make.com)

**Steps:**

1. Generate a new key: Dashboard → API Keys → Create New Key. Copy it immediately — it is only shown once.
2. Search for the old key in your codebase:

```bash
grep -r "OLD_KEY_VALUE" .
```

3. Update environment variables:

```bash
# .env
PULLBAY_API_KEY=new_key_here

# Or via secrets manager (AWS, GCP, Vault, etc.)
```

4. Redeploy, then verify:

```python
import requests
import os

api_key = os.environ.get("PULLBAY_API_KEY")
response = requests.get(
    "https://api.pullbay.com/appstore/reviews",
    params={"appId": "389801252", "maxItems": 1},
    headers={"Authorization": f"Bearer {api_key}"},
)
assert response.status_code == 200, f"Auth failed: {response.status_code}"
print("API key is working")
```

***

## Credit Issues

### Error: `402 Payment Required` / code `TEAM_INSUFFICIENT_CREDITS`

Your account has run out of API credits.

**1. Check your balance** — Dashboard → Billing → Current Balance

**2. Add credits** — Dashboard → Billing → Add Credits. Credits are added immediately after payment.

**3. Enable auto-reload** — set a threshold (e.g. reload 5,000 credits when balance drops below 1,000) so workflows never fail unexpectedly.

**4. Upgrade your plan** if you consistently run out — higher plans include larger monthly credit allocations. See the Pullbay dashboard for current plan options.

***

### Problem: Credits depleting faster than expected

**1. Check usage by endpoint** — Dashboard → Usage → Credit Usage Breakdown

**2. Understand how credits are charged**

Credits are charged per review returned, not per API call:

```
creditsCharged = reviews_returned × per_item_cost
```

`pricing.creditsCharged` in the response tells you exactly what each call cost:

```python
response = requests.get(
    "https://api.pullbay.com/appstore/reviews",
    params={"appId": "389801252", "maxItems": 500},
    headers={"Authorization": f"Bearer {API_KEY}"},
)
body = response.json()
print(f"requestId: {body['requestId']}")
print(f"Reviews returned: {len(body['data'])}")
print(f"Credits charged: {body['pricing']['creditsCharged']}")
```

**3. Reduce unnecessary fetches**

```python
# Expensive: fetches 500 reviews every hour whether new ones exist or not
# maxItems=500 → up to 500 credits per run × 24 runs/day

# Cheaper: fetch only one page (~50 reviews) to check for new content
params = {"appId": "389801252", "sort": "recent"}  # No maxItems → single page
```

**4. Cache responses** — avoid re-fetching the same data within minutes of the last call.

**5. Log credit consumption per call** — track spend over time and alert on anomalies:

```python
def call_api(params):
    response = requests.get(
        "https://api.pullbay.com/appstore/reviews",
        params=params,
        headers={"Authorization": f"Bearer {API_KEY}"},
    )
    body = response.json()
    print(
        f"requestId={body['requestId']} "
        f"reviews={len(body['data'])} "
        f"credits={body['pricing']['creditsCharged']}"
    )
    if body["pricing"]["creditsCharged"] > 200:
        print("Warning: high credit usage on this call")
    return body
```

***

## Rate Limit Issues

### Error: `429 Too Many Requests`

You have exceeded your plan's per-minute or per-second request limit.

**1. Read the rate limit headers**

```python
response = requests.get(...)

remaining = response.headers.get("X-RateLimit-Remaining")
limit      = response.headers.get("X-RateLimit-Limit")
reset_ts   = response.headers.get("X-RateLimit-Reset")

print(f"Remaining: {remaining}/{limit} requests this window")

if reset_ts:
    import time
    wait = int(reset_ts) - time.time()
    print(f"Resets in {wait:.1f}s")
```

**2. Implement exponential backoff**

```python
import time
import requests

def get_with_backoff(url, params, headers, max_retries=5):
    for attempt in range(max_retries):
        response = requests.get(url, params=params, headers=headers)

        if response.status_code == 200:
            return response

        if response.status_code == 429:
            wait = 2 ** attempt  # 1s, 2s, 4s, 8s, 16s
            print(f"Rate limited — waiting {wait}s (attempt {attempt + 1})")
            time.sleep(wait)
            continue

        response.raise_for_status()  # Raise on other errors

    raise RuntimeError("Max retries exceeded")
```

**3. Reduce request frequency**

Rate limits are per API key and depend on your plan. If you are consistently hitting limits, either reduce call frequency or upgrade to a plan with a higher rate limit.

***

### Problem: Hitting rate limits when processing multiple apps

**Option A — Add a delay between requests**

```python
import time

apps = ["389801252", "284882215", "310633997"]

for app_id in apps:
    response = requests.get(
        "https://api.pullbay.com/appstore/reviews",
        params={"appId": app_id, "maxItems": 100},
        headers={"Authorization": f"Bearer {API_KEY}"},
    )
    process(response.json())
    time.sleep(1)  # 1s gap between calls
```

**Option B — Use `maxItems` to reduce total call count**

Instead of paginating through many pages (one request per page), use `maxItems` to get up to 500 reviews in a single call:

```python
# Multiple calls (one per page)
for page in range(1, 11):
    response = requests.get(
        "https://api.pullbay.com/appstore/reviews",
        params={"appId": "389801252", "page": page},
        headers={"Authorization": f"Bearer {API_KEY}"},
    )
    ...

# Single call — Pullbay paginates internally
response = requests.get(
    "https://api.pullbay.com/appstore/reviews",
    params={"appId": "389801252", "maxItems": 500},
    headers={"Authorization": f"Bearer {API_KEY}"},
)
```

***

## Data Issues

### Problem: Empty `data` array

A `200 OK` response with `"data": []` means the request succeeded but no matching reviews exist.

**Check the app ID**

```python
# appId must be a numeric string, 8–12 digits
params = {"appId": "389801252", "maxItems": 5}  # Correct
params = {"appId": "com.burbn.instagram"}        # Wrong — use bundleId for bundle IDs
params = {"bundleId": "com.burbn.instagram"}     # Correct for bundle IDs
```

`appId` and `bundleId` are mutually exclusive — pass exactly one.

**Check the country**

Apps may have reviews in some storefronts but not others:

```python
# If US returns empty, try other countries
for country in ["us", "gb", "de", "fr", "au", "jp"]:
    response = requests.get(
        "https://api.pullbay.com/appstore/reviews",
        params={"appId": "389801252", "country": country, "maxItems": 5},
        headers={"Authorization": f"Bearer {API_KEY}"},
    )
    count = len(response.json()["data"])
    print(f"{country}: {count} reviews")
```

**Check the sort value**

Only `recent` and `helpful` are valid. Any other value causes a `400 VALIDATION_ERROR`.

***

### Problem: Wrong or unexpected field names

The response uses different field names than expected. The App Store review object fields are:

| Field      | Type    | Description                 |
| ---------- | ------- | --------------------------- |
| `id`       | string  | Unique review ID            |
| `date`     | string  | ISO 8601 datetime           |
| `userName` | string  | Reviewer display name       |
| `userUrl`  | string  | Link to reviewer profile    |
| `score`    | integer | Star rating 1–5             |
| `title`    | string  | Review headline             |
| `text`     | string  | Full review body            |
| `version`  | string  | App version reviewed        |
| `url`      | string  | Link to the review on Apple |
| `country`  | string  | 2-letter country code       |

Common mistakes:

```python
# Wrong field names from the response
review["author"]     # → use review["userName"]
review["rating"]     # → use review["score"]
review["content"]    # → use review["text"]
review["helpful_count"]  # → field does not exist
```

***

## Pagination Issues

### Problem: Getting only 50 reviews when you expect more

Without `maxItems`, the endpoint returns one page (\~50 reviews). Use `maxItems` to get up to 500 at once, or paginate with `page`:

```python
# Option 1 — Bulk fetch (up to 500 reviews, single call)
response = requests.get(
    "https://api.pullbay.com/appstore/reviews",
    params={"appId": "389801252", "maxItems": 500},
    headers={"Authorization": f"Bearer {API_KEY}"},
)
reviews = response.json()["data"]

# Option 2 — Manual page-by-page iteration
all_reviews = []
for page in range(1, 11):  # Max 10 pages
    response = requests.get(
        "https://api.pullbay.com/appstore/reviews",
        params={"appId": "389801252", "page": page},
        headers={"Authorization": f"Bearer {API_KEY}"},
    )
    body = response.json()
    all_reviews.extend(body["data"])
    if not body["pagination"]["hasNextPage"]:
        break
```

`maxItems` and `page` cannot be used in the same request — choose one.

***

### Problem: Pagination loop never stops

Check `pagination.hasNextPage` (not `has_more` or `next`):

```python
all_reviews = []
page = 1

while True:
    response = requests.get(
        "https://api.pullbay.com/appstore/reviews",
        params={"appId": "389801252", "page": page},
        headers={"Authorization": f"Bearer {API_KEY}"},
    )
    body = response.json()
    all_reviews.extend(body["data"])

    if not body["pagination"]["hasNextPage"]:
        break  # No more pages

    page += 1
    if page > 10:
        break  # Apple's RSS feed has a hard limit of 10 pages
```

***

### Problem: Request with `maxItems` times out

`maxItems` causes Pullbay to fetch multiple Apple pages internally before responding — this can take up to 2 minutes for large values.

**Increase the client timeout:**

```python
response = requests.get(
    "https://api.pullbay.com/appstore/reviews",
    params={"appId": "389801252", "maxItems": 500},
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=120,  # 2 minutes
)
```

**Or reduce `maxItems`** to fetch fewer pages internally:

```python
params={"appId": "389801252", "maxItems": 100}  # Faster than 500
```

**Or switch to page-by-page** with a shorter per-request timeout:

```python
for page in range(1, 11):
    response = requests.get(
        "https://api.pullbay.com/appstore/reviews",
        params={"appId": "389801252", "page": page},
        headers={"Authorization": f"Bearer {API_KEY}"},
        timeout=30,
    )
    body = response.json()
    process(body["data"])
    if not body["pagination"]["hasNextPage"]:
        break
```

***

## n8n / Make.com Issues

### Problem: n8n workflow fails on the HTTP Request node

**1. Check credential setup**

In n8n: Credentials → Create New → Header Auth

| Field        | Value                 |
| ------------ | --------------------- |
| Header Name  | `Authorization`       |
| Header Value | `Bearer YOUR_API_KEY` |

Include the word `Bearer` and a space before the key.

**2. Check the URL**

The correct endpoint URL is:

```
https://api.pullbay.com/appstore/reviews
```

Common mistakes:

* `/app-store/` → should be `/appstore/` (no hyphen)
* `/reviews/all` → no such endpoint; use `maxItems` parameter instead

**3. Check parameter names** — use camelCase:

| Wrong         | Correct    |
| ------------- | ---------- |
| `app_id`      | `appId`    |
| `max_results` | `maxItems` |
| `bundle_id`   | `bundleId` |

**4. Increase the HTTP timeout**

Click **Additional Options** on the HTTP Request node and set **Timeout** to `120000` ms (2 minutes). Large `maxItems` values require extra time while Pullbay fetches pages internally.

**5. Read the right response fields in n8n**

| What you need   | n8n expression                       |
| --------------- | ------------------------------------ |
| Reviews array   | `{{ $json.data }}`                   |
| Credits charged | `{{ $json.pricing.creditsCharged }}` |
| Request ID      | `{{ $json.requestId }}`              |
| Has next page   | `{{ $json.pagination.hasNextPage }}` |
| Author name     | `{{ $json.userName }}`               |
| Score           | `{{ $json.score }}`                  |
| Review text     | `{{ $json.text }}`                   |

**6. Handle `429` with a Wait node**

Connect a `Wait` node (set to 60 seconds) after the HTTP Request node on the error path, then loop back to retry.

***

## General Debugging Steps

{% stepper %}
{% step %}

## Save the `requestId`

Every response (success and error) includes a top-level `requestId`. Log it for every call — it is the key piece of information when contacting support:

```python
body = response.json()
print(f"requestId: {body['requestId']}")
print(f"success:   {body['success']}")
if not body["success"]:
    print(f"error:     {body['error']['code']}")
```

{% endstep %}

{% step %}

## Check Pullbay service health

If you are seeing 5xx errors, check the Pullbay status page (linked in the dashboard) for known incidents before debugging your code.
{% endstep %}

{% step %}

## Reproduce with curl

Strip everything back to a minimal curl call to isolate whether the problem is in your code or the API:

```bash
curl -X GET \
  "https://api.pullbay.com/appstore/reviews?appId=389801252&maxItems=5" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

* `200` with data → API is fine; the bug is in your integration
* `401` → invalid API key
* `402` → insufficient credits
* `400` with `VALIDATION_ERROR` → a parameter name or value is wrong
* `5xx` → server-side issue; check the status page
  {% endstep %}

{% step %}

## Verify request parameters

```python
# appId: 8–12 digit numeric string
# bundleId: reverse-domain string (e.g. com.example.app)
# country: exactly 2 letters (e.g. "us", "gb")
# sort: "recent" or "helpful" only
# maxItems: integer 1–500
# page: integer 1–10
# maxItems and page are mutually exclusive
# appId and bundleId are mutually exclusive
```

{% endstep %}

{% step %}

## Check your credit balance

Dashboard → Billing → Current Balance. A balance of 0 causes every request to return `402`.
{% endstep %}

{% step %}

## Review rate limit headers

```python
print("Remaining:", response.headers.get("X-RateLimit-Remaining"))
print("Limit:",     response.headers.get("X-RateLimit-Limit"))
print("Reset at:",  response.headers.get("X-RateLimit-Reset"))
```

{% endstep %}

{% step %}

## Contact support

If still unresolved, open a support ticket and include:

* [ ] The `requestId` from the error response
* [ ] The exact endpoint and parameters you called
* [ ] The full error response body (status code + `error.code`)
* [ ] When the issue started
* [ ] Steps you have already tried
  {% endstep %}
  {% endstepper %}

## FAQ

<details>

<summary>How do I tell if the problem is my code or Pullbay's servers?</summary>

* `4xx` errors (except `429`) — your request is malformed (auth, parameters, credits)
* `429` — your request rate is too high for your plan
* `5xx` — server-side issue; check the status page and retry with backoff
* Network errors — usually transient; retry with backoff

</details>

<details>

<summary>What is a <code>requestId</code> and why does it matter?</summary>

Every response contains a `requestId` (also returned as the `X-Request-Id` response header). It uniquely identifies the request in Pullbay's logs. Always log it — without it, support cannot trace what happened.

</details>

<details>

<summary>How can I reduce credit consumption?</summary>

* Use `maxItems` only as large as you need — credits are charged per review returned
* Cache responses; avoid fetching the same app repeatedly within a short window
* Fetch a single page (no `maxItems`, no `page`) when you only need the latest reviews

</details>

<details>

<summary>Is there a sandbox or test mode?</summary>

There is no separate sandbox. To minimise credit usage during development, test with `maxItems=5` to fetch only 5 reviews and verify your workflow is correct before scaling up.

</details>

<details>

<summary>Can I filter reviews by date or rating in the API?</summary>

No — the App Store reviews endpoint does not support `from_date`, `to_date`, or `rating` filter parameters. Fetch reviews and apply date or score filters in your own code after receiving the response.

</details>


# FAQ

Welcome to Pullbay! This FAQ covers common questions about getting started, billing, integrations, and more.

***

## Getting Started

<details>

<summary>What is Pullbay?</summary>

Pullbay is a data API platform that provides access to structured data from app stores, social platforms, and other services. Instead of scraping or building complex integrations yourself, Pullbay handles data fetching, normalisation, and delivery through a simple REST API.

**Use Pullbay for:**

* App Store reviews, ratings, and metadata
* App Store analytics and tracking
* Automating workflows with n8n, Make.com, and other automation platforms
* Building products that need App Store data

**Why Pullbay over scraping:**

* No scraping required — guaranteed data freshness
* Structured, standardised response format
* Reliable uptime and API support
* Credit-based pricing so you only pay for what you use

</details>

<details>

<summary>Do I need a credit card to sign up?</summary>

No. Pullbay offers a free tier with no credit card required. You get a credit allocation to explore the API. Upgrade to a paid plan when you are ready to scale.

</details>

<details>

<summary>How do I get my API key?</summary>

1. Create a Pullbay account at [pullbay.com](https://pullbay.com/)
2. Log in to your [dashboard](https://dashboard.pullbay.com/)
3. Navigate to **API Keys** in the left sidebar
4. Click **Create New Key**
5. Give the key a name (e.g. "Production", "Development")
6. Copy the key and store it securely — it is only shown once

**Never share your API key.** Treat it like a password. If you accidentally expose it, revoke it immediately from the API Keys page and create a new one.

</details>

<details>

<summary>Can I use multiple API keys?</summary>

Yes. You can create as many keys as you need from the dashboard:

1. Go to **API Keys** → **Create New Key**
2. Create separate keys for different applications, team members, environments (development, staging, production), or services

Each key has its own rate limit window. Revoking one key does not affect others — this is useful for isolating access and rotating keys safely.

</details>

<details>

<summary>How do I revoke an API key?</summary>

If you accidentally expose a key:

1. Go to **API Keys** in the dashboard
2. Find the exposed key
3. Click the three-dot menu → **Revoke**
4. The old key becomes invalid immediately
5. Update all applications to use a new key before revoking the old one

</details>

***

## Credits & Billing

<details>

<summary>How does credit billing work?</summary>

Pullbay uses a credit-based billing system:

1. **Sign up for a plan** — choose Free, Starter, Growth, or Scale
2. **Receive credits** — your plan includes a monthly credit allowance
3. **Use the API** — each successful request deducts credits based on the number of records returned
4. **Top up when needed** — add credits anytime from the dashboard
5. **Renew monthly** — plan credits reset on your billing date

`pricing.creditsCharged` in every API response tells you exactly how many credits that call consumed.

</details>

<details>

<summary>Are credits charged for failed requests?</summary>

No. Credits are only deducted for successful responses (`2xx`):

* `4xx` client errors (400, 401, 403, 404) — not charged
* `5xx` server errors (500, 502, 503) — not charged
* `429 Too Many Requests` — not charged
* Timeout errors — not charged

Only requests that return data consume credits.

</details>

<details>

<summary>Do credits expire?</summary>

**Plan credits** reset monthly — unused credits from the previous month are lost on your billing date.

**Purchased top-up credits** never expire. They are used after your monthly plan credits are exhausted and carry forward indefinitely.

**Strategy:** Rely on plan credits for predictable usage, and keep a top-up balance as a buffer for unexpected spikes.

</details>

<details>

<summary>What happens when I run out of credits?</summary>

When your balance reaches 0:

* New requests return `402 Payment Required` with error code `TEAM_INSUFFICIENT_CREDITS`
* No suspension — the API remains accessible, you just cannot make requests
* All your historical data and settings remain intact

To resume:

1. Dashboard → **Billing** → **Add Credits**
2. Choose an amount and complete payment
3. Credits are added immediately (usually within seconds)
4. Retry your requests

To prevent future outages, set up **auto-reload** in billing settings to automatically top up when your balance drops below a threshold.

</details>

<details>

<summary>Can I get a refund for credits?</summary>

Credits are non-refundable per Pullbay's Terms of Service. However:

* Unused credits persist in your account indefinitely (top-up credits)
* If a Pullbay error caused incorrect credit charges, contact support — the team may credit your account
* For special circumstances, email support with your account email, a description of the issue, and relevant `requestId` values

</details>

<details>

<summary>How much do credits cost?</summary>

Credit pricing and plan allocations are shown in the [Pullbay dashboard](https://dashboard.pullbay.com/) under **Billing**.

</details>

<details>

<summary>Which plan should I choose?</summary>

Choose based on your request volume and rate limit needs:

| Plan        | Best For                       |
| ----------- | ------------------------------ |
| **Free**    | Learning, testing, exploration |
| **Hobby**   | Small apps, side projects      |
| **Builder** | Production applications        |
| **Scale**   | High-volume, multiple apps     |

Start with the free tier and upgrade when you need higher rate limits or more monthly credits.

</details>

***

## Rate Limits

<details>

<summary>What are the rate limits?</summary>

Rate limits are enforced per API key on a per-second and per-minute basis. The exact limits depend on your plan. Check your current limits in the dashboard under **Account → Plan**.

When you exceed your rate limit, the API returns `429 Too Many Requests`. No credits are charged for rate-limited requests.

</details>

<details>

<summary>What happens when I hit the rate limit?</summary>

```http
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1729902400
```

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 429,
  "message": "Too Many Requests",
  "success": false,
  "error": { "code": "RATE_LIMIT_EXCEEDED" }
}
```

Read `X-RateLimit-Reset` to know when the window resets, then retry.

</details>

<details>

<summary>How do I handle rate limits in my code?</summary>

```python
import requests
import time

def call_with_retry(url, params, api_key, max_retries=5):
    for attempt in range(max_retries):
        response = requests.get(
            url,
            params=params,
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=30,
        )

        if response.status_code == 200:
            return response.json()

        if response.status_code == 429:
            reset_ts = int(response.headers.get("X-RateLimit-Reset", 0))
            wait     = max(1, reset_ts - time.time() + 1)
            print(f"Rate limited — waiting {wait:.1f}s")
            time.sleep(wait)
            continue

        if response.status_code >= 500:
            wait = 2 ** attempt
            print(f"Server error — retrying in {wait}s")
            time.sleep(wait)
            continue

        response.raise_for_status()

    raise RuntimeError("Max retries exceeded")
```

**Pre-emptive check:** Monitor `X-RateLimit-Remaining` on every response and slow down before hitting 0.

</details>

<details>

<summary>Can I get a higher rate limit?</summary>

Yes:

* **Upgrade your plan** — each plan tier increases the rate limit
* **Contact sales** — custom limits are available for Scale customers

</details>

***

## App Store Service

<details>

<summary>What App Store data can I access?</summary>

Pullbay provides access to:

* **Reviews** — user reviews, scores, titles, text, reviewer name, version, country, date
* **App metadata** — name, category, release date, version, size, developer info, ratings, screenshots
* **Similar apps** — apps related to a given app
* **Developer apps** — all apps published by a developer
* **Search** — find apps by keyword

</details>

<details>

<summary>How do I find an app's ID?</summary>

Apps can be identified by their **numeric App Store ID** (`appId`) or their **bundle ID** (`bundleId`) — use exactly one per request.

**Find the numeric ID from the App Store URL:**

```
https://apps.apple.com/us/app/instagram/id389801252
                                               ↑
                                     appId: 389801252
```

**Search by keyword using the Pullbay search endpoint:**

```python
import requests

response = requests.get(
    "https://api.pullbay.com/appstore/search",
    params={"term": "Instagram", "country": "us"},
    headers={"Authorization": f"Bearer {API_KEY}"},
)

for app in response.json()["data"]:
    print(f"{app['title']}: appId={app['id']}, bundleId={app['appId']}")
```

**Common numeric IDs:**

* Instagram: `389801252`
* TikTok: `835599320`
* X (Twitter): `333903271`
* YouTube: `544007664`

</details>

<details>

<summary>What countries are supported for App Store reviews?</summary>

Reviews are available for any country supported by the App Store, specified as a 2-letter ISO 3166-1 alpha-2 code:

| Code | Country        |
| ---- | -------------- |
| `us` | United States  |
| `gb` | United Kingdom |
| `de` | Germany        |
| `fr` | France         |
| `jp` | Japan          |
| `au` | Australia      |
| `ca` | Canada         |
| `in` | India          |

Pass the code as the `country` parameter (default: `us`):

```python
response = requests.get(
    "https://api.pullbay.com/appstore/reviews",
    params={"appId": "389801252", "country": "gb", "sort": "recent"},
    headers={"Authorization": f"Bearer {API_KEY}"},
)
```

</details>

<details>

<summary>How fresh is the review data?</summary>

Data is fetched from Apple's API on demand when you make a request. There is no Pullbay-side cache — each call hits Apple directly.

In practice, Apple's own RSS feeds have a lag of 1–24 hours from when a review is published to when it appears in the feed. This is an Apple limitation, not a Pullbay limitation.

</details>

<details>

<summary>Can I filter reviews by date or rating?</summary>

No — the App Store reviews endpoint does not support `since`, `until`, or rating filter parameters. Fetch reviews and apply any date or score filters in your own code:

```python
from datetime import datetime, timezone

response = requests.get(
    "https://api.pullbay.com/appstore/reviews",
    params={"appId": "389801252", "maxItems": 500, "sort": "recent"},
    headers={"Authorization": f"Bearer {API_KEY}"},
)

reviews = response.json()["data"]

# Filter to last 7 days
cutoff = datetime.now(timezone.utc).timestamp() - 7 * 86400
recent = [r for r in reviews if datetime.fromisoformat(r["date"]).timestamp() > cutoff]

# Filter to low scores
low_score = [r for r in reviews if r["score"] <= 2]
```

</details>

<details>

<summary>What is the maximum number of reviews I can fetch?</summary>

The App Store review feed is limited to 10 pages of \~50 reviews each — a maximum of **500 reviews** per app per country.

Use `maxItems` (1–500) to receive all available reviews in a single response:

```python
response = requests.get(
    "https://api.pullbay.com/appstore/reviews",
    params={"appId": "389801252", "maxItems": 500},
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=120,  # Allow time for internal pagination
)
reviews = response.json()["data"]  # Up to 500 reviews
```

</details>

***

## Pagination

<details>

<summary>What are the two fetch modes for reviews?</summary>

The `/api/appstore/reviews` endpoint supports two mutually exclusive modes:

| Mode            | Parameter             | Behaviour                                                                                                        |
| --------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------- |
| **Bulk**        | `maxItems=1–500`      | Pullbay paginates Apple's API internally and returns all results in one response. No pagination fields returned. |
| **Single page** | `page=1–10` (or omit) | Returns \~50 reviews for that page with `pagination.hasNextPage` and `pagination.page`.                          |

`maxItems` and `page` cannot be used in the same request.

</details>

<details>

<summary>How do I paginate page-by-page?</summary>

```python
all_reviews = []
page = 1

while True:
    response = requests.get(
        "https://api.pullbay.com/appstore/reviews",
        params={"appId": "389801252", "page": page},
        headers={"Authorization": f"Bearer {API_KEY}"},
        timeout=30,
    )
    body = response.json()
    all_reviews.extend(body["data"])

    if not body["pagination"]["hasNextPage"]:
        break

    page += 1
    if page > 10:
        break  # Apple's feed is capped at 10 pages
```

</details>

<details>

<summary>Can I resume pagination if my request fails?</summary>

Yes. Because the App Store uses page-based pagination, store the last successfully processed page number and resume from there:

```python
import json
import os

STATE_FILE = "pagination_state.json"

def load_state():
    if os.path.exists(STATE_FILE):
        return json.load(open(STATE_FILE))
    return {"last_page": 0}

def save_state(page):
    json.dump({"last_page": page}, open(STATE_FILE, "w"))

state    = load_state()
start    = state["last_page"] + 1
reviews  = []

for page in range(start, 11):
    try:
        response = requests.get(
            "https://dashboard.pullbay.com/api/appstore/reviews",
            params={"appId": "389801252", "page": page},
            headers={"Authorization": f"Bearer {API_KEY}"},
            timeout=30,
        )
        body = response.json()
        reviews.extend(body["data"])
        save_state(page)  # Save progress

        if not body["pagination"]["hasNextPage"]:
            break
    except Exception as e:
        print(f"Failed on page {page}: {e}")
        break  # Next run resumes from saved page
```

</details>

***

## Integrations

<details>

<summary>Can I use Pullbay with n8n?</summary>

Yes. Use the **HTTP Request** node with these settings:

* **Method:** GET
* **URL:** `https://api.pullbay.com/appstore/reviews`
* **Authentication:** Header Auth (`Authorization: Bearer YOUR_API_KEY`)
* **Query Parameters:** `appId`, `country`, `sort`, `maxItems` (or `page`)

See the n8n Integration Guide for complete workflow examples including Google Sheets sync, Slack alerts, and sentiment analysis.

</details>

<details>

<summary>Can I use Pullbay with Make.com or Zapier?</summary>

Use the **HTTP module** (Make.com) or **Webhooks by Zapier** with the same endpoint and parameters as above. Dedicated Make.com and Zapier modules are in development.

</details>

<details>

<summary>Should I call Pullbay from client-side code?</summary>

**No — never from client-side JavaScript (browser or mobile app).**

If your API key is in client-side code, anyone can view it in browser DevTools, steal your key, and consume your credits.

**Correct architecture:**

```
Browser → Your backend → Pullbay API
```

Your backend holds the API key and proxies requests:

```javascript
// backend — api key stays on the server
app.get("/api/reviews/:appId", async (req, res) => {
  const response = await fetch(
    `https://api.pullbay.com/appstore/reviews?appId=${req.params.appId}&maxItems=50`,
    { headers: { Authorization: `Bearer ${process.env.PULLBAY_API_KEY}` } }
  );
  res.json(await response.json());
});
```

```javascript
// frontend — no API key here
fetch("/api/reviews/389801252")
  .then(r => r.json())
  .then(data => console.log(data.data));
```

</details>

***

## Security & Compliance

<details>

<summary>Is the data Pullbay provides legal to use?</summary>

Pullbay provides access to publicly available data. However, **you are responsible** for ensuring your specific use case complies with Apple's Terms of Service and applicable law.

Generally acceptable uses: analytics, app monitoring, market research, competitor analysis.

Consult your legal team for commercial use cases involving large-scale data collection or redistribution.

</details>

<details>

<summary>How do I keep my API key secure?</summary>

**Do:**

* Store in environment variables — never in source code
* Use a secrets manager in production (AWS Secrets Manager, Google Secret Manager, HashiCorp Vault)
* Create separate keys per application and environment
* Revoke and rotate keys regularly

**Never:**

* Commit a key to version control
* Share in email, Slack, or chat
* Include in client-side JavaScript, mobile app bundles, or public repositories
* Hardcode in any source file

```python
import os
api_key = os.environ["PULLBAY_API_KEY"]  # Correct
api_key = "live_abc123..."               # Never do this
```

</details>

<details>

<summary>Does Pullbay store the data I fetch?</summary>

No. Data is fetched from upstream sources (Apple's API) on demand and returned directly to you. Pullbay does not persist the content of your responses.

Pullbay does store:

* API usage logs (request timestamp, endpoint, status code, credits charged) — for billing and support
* Your account settings and API keys
* Aggregated usage metrics

Usage logs are retained for billing and support purposes (typically 90 days).

</details>

***

## Support

<details>

<summary>How do I contact support?</summary>

**Email:** <support@pullbay.com>

Include in your request:

* The `requestId` from the error response (this is the fastest way for support to find your request in logs)
* The endpoint and parameters you called
* The full error response (status code + `error.code`)
* Steps to reproduce
* What you expected vs. what happened

**Response times:**

| Plan    | Response Time            |
| ------- | ------------------------ |
| Free    | 2–3 business days        |
| Starter | 1 business day           |
| Growth  | 24 hours                 |
| Scale   | 2 hours (business hours) |

</details>

<details>

<summary>What should I include in a support request?</summary>

The `requestId` field is returned in every response — success and error alike. It is also available as the `X-Request-Id` response header. Log it for every API call:

```python
body = response.json()
print(f"requestId: {body['requestId']}")
```

**Example support request:**

```
Subject: 402 error on reviews endpoint — unexpected credit depletion

requestId: 3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24
Endpoint: GET https://api.pullbay.com/appstore/reviews
Params:   appId=389801252, maxItems=500

Error: HTTP 402
Body: {"error": {"code": "TEAM_INSUFFICIENT_CREDITS"}}

Steps to reproduce:
1. Ran 20 requests this morning, all with maxItems=500
2. Request #18 returned 402

Expected: requests to succeed
Actual: balance depleted faster than expected

Can you help me understand how many credits were consumed per call?
```

</details>

<details>

<summary>What if I have an urgent production issue?</summary>

Email support immediately with **"URGENT"** in the subject line. Include:

* The `requestId` values showing the issue
* Current customer impact (how many users affected)
* Business impact
* Any workaround you have in place

Scale plan customers may have access to a priority support channel — ask your account manager.

</details>

***

## Troubleshooting

<details>

<summary>I am getting `401 Unauthorized`</summary>

**Cause:** Authentication failed.

**Fix:**

1. Verify your key exists and is active: Dashboard → API Keys
2. Check the header format — it must be exactly `Authorization: Bearer YOUR_KEY` (capital B, space after Bearer)
3. Test with curl:

```bash
curl "https://api.pullbay.com/appstore/reviews?appId=389801252&maxItems=1" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

</details>

<details>

<summary>I am getting `402 Payment Required`</summary>

**Cause:** Your credit balance is 0.

**Fix:**

1. Check balance: Dashboard → Billing → Current Balance
2. Add credits: Dashboard → Billing → Add Credits
3. Credits are added immediately — retry your request

</details>

<details>

<summary>I am getting `429 Too Many Requests`</summary>

**Cause:** You exceeded your plan's rate limit.

**Fix:**

1. Read `X-RateLimit-Reset` from the response header and wait until that timestamp
2. Implement exponential backoff (see Rate Limits section)
3. Upgrade your plan for a higher rate limit

</details>

<details>

<summary>I am getting `400 Bad Request` with `VALIDATION_ERROR`</summary>

**Cause:** A parameter name or value is invalid.

**Common mistakes:**

| Wrong                         | Correct                               |
| ----------------------------- | ------------------------------------- |
| `app_id`                      | `appId`                               |
| `max_results`                 | `maxItems`                            |
| `bundle_id`                   | `bundleId`                            |
| `sort: "critical"`            | `sort: "recent"` or `sort: "helpful"` |
| `appId` + `bundleId` together | use exactly one                       |
| `maxItems` + `page` together  | use exactly one                       |

</details>

<details>

<summary>I am getting `5xx` server errors</summary>

**Cause:** A Pullbay server issue.

**Fix:**

1. Check the Pullbay status page for known incidents: [status.pullbay.com](https://status.pullbay.com/)
2. Retry with exponential backoff
3. Contact support if errors persist — include the `requestId`

</details>

<details>

<summary>Requests are timing out</summary>

**Cause:** The request took longer than your client timeout.

**Fix:**

* For `maxItems=500`: set client timeout to at least `120` seconds — Pullbay internally fetches up to 10 Apple pages
* For single-page requests (`page=N`): `30` seconds is sufficient

```python
response = requests.get(url, params=params, headers=headers, timeout=120)
```

</details>

<details>

<summary>I am getting an empty `data` array</summary>

**Cause:** The request succeeded but no reviews match.

**Check:**

1. `appId` is a valid 8–12 digit numeric string (or use `bundleId` for bundle IDs)
2. The app has reviews in the requested `country`
3. You are not passing both `appId` and `bundleId` in the same request

```python
# Try the US store first; if empty, try other countries
for country in ["us", "gb", "au", "ca"]:
    body = requests.get(
        "https://api.pullbay.com/appstore/reviews",
        params={"appId": "389801252", "country": country, "maxItems": 5},
        headers={"Authorization": f"Bearer {API_KEY}"},
    ).json()
    print(f"{country}: {len(body['data'])} reviews")
```

</details>

***

## Miscellaneous

<details>

<summary>How do I report a bug?</summary>

Email <support@pullbay.com> with:

* Description of the unexpected behaviour
* Steps to reproduce
* Expected vs. actual result
* `requestId` values (if API-related)

</details>

<details>

<summary>Can I request a feature?</summary>

Yes — use the feedback form in the dashboard or email support. Include the feature description, your use case, and how it would improve your workflow.

</details>

<details>

<summary>Where can I find API documentation?</summary>

Full endpoint documentation, request schemas, and response examples are available in the [Pullbay dashboard](https://dashboard.pullbay.com/) under **Docs**.

</details>

<details>

<summary>How do I stay updated?</summary>

* **Status page:** [status.pullbay.com](https://status.pullbay.com/) — incidents and maintenance
* **Dashboard notifications:** major feature releases are announced in the dashboard
* **Email:** subscribe to release notes under Dashboard → Settings → Email Preferences

</details>

***

## Still have questions?

**Email:** <support@pullbay.com>

Include the `requestId` from your error response — it lets support find your request in logs immediately and resolve issues much faster.


# How-To Guides for Google Maps


# How to Fetch Google Maps Reviews from a Place URL

Google Maps reviews are one of the most useful sources for understanding customer feedback, local reputation, and competitor performance. But manually reading reviews from Google Maps does not work well when you need structured review data for dashboards, reports, or monitoring workflows.

Pullbay’s Google Maps Reviews endpoint lets you fetch reviews for a place using the full Google Maps place URL.

{% hint style="warning" %}

## Important: Reviews Require `placeUrl`, Not `fid`

Pullbay’s Google Maps API uses different identifiers for different endpoints:

* `fid` is used for `GET /google-maps/place/{fid}`
* `placeUrl` is used for `GET /google-maps/place/reviews`

The reviews endpoint requires the full Google Maps place URL. Do not pass an FID to the reviews endpoint.
{% endhint %}

## Endpoint

```
GET https://api.pullbay.com/google-maps/place/reviews

Authorization: Bearer YOUR_API_KEY
```

## Parameters

| Parameter | Type    | Required | Description                              |
| --------- | ------- | -------- | ---------------------------------------- |
| placeUrl  | string  | Yes      | Full Google Maps place URL               |
| maxItems  | integer | No       | One-shot bulk pull, 1–500 results        |
| cursor    | string  | No       | Cursor returned by the previous response |

`maxItems` and `cursor` are mutually exclusive. Use one or the other in a request.

## Approaches

{% tabs %}
{% tab title="Fetch Reviews with maxItems" %}
Use `maxItems` when you want a simple one-shot pull.

```python
import requests

API_KEY = "YOUR_API_KEY"

BASE_URL = "https://api.pullbay.com"

headers = {
    "Authorization": f"Bearer {API_KEY}"
}

place_url = "https://www.google.com/maps/place/example-place-url"

params = {
    "placeUrl": place_url,
    "maxItems": 100
}

response = requests.get(
    f"{BASE_URL}/google-maps/place/reviews",
    headers=headers,
    params=params
)

response.raise_for_status()

data = response.json()
reviews = data.get("data", [])
credits = data.get("pricing", {}).get("creditsCharged")

print(f"Fetched {len(reviews)} reviews")
print(f"Credits charged: {credits}")

for review in reviews:
    print(review.get("stars"), review.get("dateIso"))
    print(review.get("reviewer", {}).get("name"))
    print(review.get("text"))
    print("---")
```

### When to use this

Use this approach when you need up to 500 reviews in a single request and do not need to continue with cursor pagination.
{% endtab %}

{% tab title="Cursor Pagination for More Review Pulls" %}
The reviews endpoint uses cursor-style pagination. The response can include `pagination.cursor`. Pass that cursor in the next request to continue.

```python
import requests

API_KEY = "YOUR_API_KEY"

BASE_URL = "https://api.pullbay.com"

headers = {
    "Authorization": f"Bearer {API_KEY}"
}

place_url = "https://www.google.com/maps/place/example-place-url"

all_reviews = []
cursor = None

while True:
    params = {
        "placeUrl": place_url
    }

    if cursor:
        params["cursor"] = cursor
    else:
        params["maxItems"] = 100

    response = requests.get(
        f"{BASE_URL}/google-maps/place/reviews",
        headers=headers,
        params=params
    )

    response.raise_for_status()

    data = response.json()
    reviews = data.get("data", [])

    all_reviews.extend(reviews)

    print(f"Fetched {len(reviews)} reviews")

    cursor = data.get("pagination", {}).get("cursor")

    if not cursor:
        break

print(f"Total reviews fetched: {len(all_reviews)}")
```

{% endtab %}
{% endtabs %}

## Useful Review Fields

A Google Maps review can include:

| Field                 | Description                                   |
| --------------------- | --------------------------------------------- |
| id                    | Review ID                                     |
| link                  | Permalink to the review                       |
| text                  | Review body                                   |
| stars                 | Rating from 1 to 5                            |
| date                  | Posted timestamp in Unix milliseconds         |
| dateIso               | Posted timestamp in ISO 8601 format           |
| likes                 | Number of likes on the review, nullable       |
| photos                | Photo URLs attached to the review             |
| reviewer.name         | Reviewer display name                         |
| reviewer.url          | Reviewer Google Maps profile URL              |
| reviewer.totalReviews | Total reviews written by the reviewer         |
| reviewer.totalPhotos  | Total photos uploaded by the reviewer         |
| ownersResponse        | Owner response object, empty object when none |
| details               | Free-form review metadata                     |

## Client-Side Filtering by Rating

The API does not document a server-side rating filter for reviews. If you want only low-rating reviews, fetch reviews first and filter by stars in your own code.

```python
low_rating_reviews = [
    review for review in reviews
    if review.get("stars") in [1, 2]
]

print(f"Low-rating reviews: {len(low_rating_reviews)}")
```

## Export Reviews to CSV

```python
import csv

with open("google_maps_reviews.csv", "w", newline="", encoding="utf-8") as file:
    writer = csv.DictWriter(
        file,
        fieldnames=[
            "id",
            "stars",
            "dateIso",
            "reviewerName",
            "reviewerUrl",
            "text",
            "likes",
            "link"
        ]
    )

    writer.writeheader()

    for review in reviews:
        reviewer = review.get("reviewer") or {}

        writer.writerow({
            "id": review.get("id"),
            "stars": review.get("stars"),
            "dateIso": review.get("dateIso"),
            "reviewerName": reviewer.get("name"),
            "reviewerUrl": reviewer.get("url"),
            "text": review.get("text"),
            "likes": review.get("likes"),
            "link": review.get("link")
        })

print("Saved reviews to google_maps_reviews.csv")
```

## Deduplication

If you fetch reviews repeatedly, store the review id and use it as a unique key.

```python
seen_review_ids = set()
unique_reviews = []

for review in all_reviews:
    review_id = review.get("id")

    if review_id and review_id not in seen_review_ids:
        seen_review_ids.add(review_id)
        unique_reviews.append(review)
```

## Credit Considerations

Each request is billed independently. The exact credit charge is returned in:

```python
data["pricing"]["creditsCharged"]
```

Credit cost is dynamic, so read it from every response instead of assuming a fixed rate.

## Troubleshooting

| Problem                  | Possible Cause                    | Fix                                               |
| ------------------------ | --------------------------------- | ------------------------------------------------- |
| 400 Bad request          | Missing or invalid placeUrl       | Make sure you pass the full Google Maps place URL |
| 400 Bad request          | maxItems and cursor used together | Use only one per request                          |
| 402 Insufficient credits | Not enough credits                | Check Pullbay account credits                     |
| 404 Not found            | Review set not found              | Verify the place URL                              |
| No cursor returned       | No continuation cursor available  | Stop pagination                                   |

## Next Steps

After fetching Google Maps reviews, you can:

* Store reviews in a database
* Deduplicate reviews by id
* Filter client-side by stars
* Track new reviews over time
* Combine reviews with place details from the fid endpoint


# How to Find Local Competitors on Google Maps

Local competitor research often starts with a simple Google Maps search: “dentist near me”, “flower shop in istanbul”, “coffee shop in brooklyn”, or “pilates studio in berlin”. The problem is that manual research does not scale when you need structured competitor data across many categories, cities, or neighborhoods.

Pullbay’s Google Maps API lets you search places by keyword and collect structured business listing data such as name, address, website, phone, rating, review count, categories, opening hours, Google Maps URL, and FID.

This guide shows how to find local competitors on Google Maps using Pullbay.

### The Competitor Research Workflow

A simple local competitor workflow looks like this:

1. Search Google Maps by business category and location
2. Collect structured place results
3. Deduplicate places by fid or placeId
4. Compare rating, review count, categories, address, website, and phone
5. Optionally fetch full place details by fid
6. Optionally fetch reviews by Google Maps place URL

### Endpoint

```
GET https://api.pullbay.com/google-maps/place/search

Authorization: Bearer YOUR_API_KEY
```

### Basic Competitor Search

```python
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.pullbay.com"

headers = {
    "Authorization": f"Bearer {API_KEY}"
}

params = {
    "query": "flower shop in istanbul",
    "countryCode": "tr",
    "languageCode": "en",
    "maxItems": 50
}

response = requests.get(
    f"{BASE_URL}/google-maps/place/search",
    headers=headers,
    params=params
)

response.raise_for_status()
data = response.json()
competitors = data.get("data", [])

for place in competitors:
    print({
        "name": place.get("title"),
        "address": place.get("address"),
        "rating": place.get("totalScore"),
        "reviews": place.get("reviewsCount"),
        "website": place.get("website"),
        "phone": place.get("phone"),
        "categories": place.get("categories"),
        "fid": place.get("fid"),
        "maps_url": place.get("url")
    })
```

### Search Multiple Categories or Areas

For broader competitor research, loop through multiple search queries.

```python
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.pullbay.com"

headers = {
    "Authorization": f"Bearer {API_KEY}"
}

queries = [
    "flower shop in istanbul",
    "online flower delivery istanbul",
    "florist in kadikoy",
    "florist in besiktas"
]

seen_fids = set()
competitors = []

for query in queries:
    params = {
        "query": query,
        "countryCode": "tr",
        "languageCode": "en",
        "maxItems": 50
    }

    response = requests.get(
        f"{BASE_URL}/google-maps/place/search",
        headers=headers,
        params=params
    )

    response.raise_for_status()
    data = response.json()

    for place in data.get("data", []):
        fid = place.get("fid")
        if fid and fid not in seen_fids:
            seen_fids.add(fid)
            competitors.append({
                "query": query,
                "title": place.get("title"),
                "address": place.get("address"),
                "city": place.get("city"),
                "rating": place.get("totalScore"),
                "reviews": place.get("reviewsCount"),
                "website": place.get("website"),
                "phone": place.get("phone"),
                "categories": place.get("categories"),
                "fid": fid,
                "url": place.get("url")
            })

print(f"Unique competitors found: {len(competitors)}")
```

#### When to use this

Use this approach when you want to map competitors across multiple search terms, neighborhoods, or service categories.

### Optional: Bias Competitor Search Toward a Map Viewport

If your research is focused on a specific map area, use latitude, longitude, and zoom.

```python
params = {
    "query": "dentist",
    "countryCode": "us",
    "languageCode": "en",
    "latitude": "40.730610",
    "longitude": "-73.935242",
    "zoom": "13",
    "maxItems": 100
}
```

These parameters bias the search toward the provided viewport.

### Save Competitor Results to CSV

```python
import csv

with open("google_maps_competitors.csv", "w", newline="", encoding="utf-8") as file:
    writer = csv.DictWriter(
        file,
        fieldnames=[
            "query",
            "title",
            "address",
            "city",
            "rating",
            "reviews",
            "website",
            "phone",
            "categories",
            "fid",
            "url"
        ]
    )

    writer.writeheader()

    for competitor in competitors:
        row = competitor.copy()
        row["categories"] = ", ".join(row.get("categories") or [])
        writer.writerow(row)

print("Saved competitors to google_maps_competitors.csv")
```

### What to Compare

Once you have competitor data, you can compare:

| Field        | Why It Matters                                      |
| ------------ | --------------------------------------------------- |
| totalScore   | Shows average customer rating                       |
| reviewsCount | Shows review volume                                 |
| categories   | Shows how the business is categorized               |
| website      | Helps identify businesses with or without a website |
| phone        | Useful for lead enrichment or contact research      |
| address      | Useful for local market mapping                     |
| location     | Useful for plotting places on your own map          |
| openingHours | Useful for comparing availability                   |

### Important Identifier Rules

Pullbay’s Google Maps endpoints use different identifiers:

* Use `fid` to fetch full place details with `/google-maps/place/{fid}`
* Use `url` as `placeUrl` to fetch reviews with `/google-maps/place/reviews`

Do not pass an FID to the reviews endpoint. Reviews require the full Google Maps place URL.

### Credit Considerations

Every response includes:

```
pricing.creditsCharged
```

Credit cost is dynamic. Store this value if you want to track usage per query.

### Troubleshooting

| Problem                  | Possible Cause                                          | Fix                                     |
| ------------------------ | ------------------------------------------------------- | --------------------------------------- |
| Duplicate businesses     | Same place appears across multiple queries              | Deduplicate with fid or placeId         |
| Missing website or phone | Field may be unavailable for that place                 | Treat nullable fields as optional       |
| 400 Bad request          | Invalid parameter or incompatible pagination parameters | Do not use offset and maxItems together |
| 402 Insufficient credits | Not enough Pullbay credits                              | Check account credits                   |
| 404 Not found            | Place or review set not found                           | Try another query or place URL          |

### Next Steps

After building your local competitor list, you can:

* Fetch full place details by fid
* Fetch customer reviews using each place’s Google Maps url
* Store competitors in SQLite, Postgres, BigQuery, or your BI workflow
* Repeat the same queries over time to monitor changes


# How to Search Google Maps Places by Keyword

Searching Google Maps manually works for a few places, but it quickly becomes slow when you need structured business data for local SEO, market research, sales prospecting, or competitor analysis. If you want to search Google Maps places by keyword and use the results in your own dashboard, spreadsheet, or database, Pullbay gives you a simple API endpoint for programmatic place search.

This guide shows how to use Pullbay’s Google Maps Place Search endpoint to search by free-text query, optionally bias results toward a country, language, or map viewport, and handle larger result sets with `maxItems` or `offset` pagination.

### The Challenge of Keyword-Based Google Maps Search

Google Maps search is useful because people search in natural language: “pizza near soho new york”, “best dentist in berlin”, or “coffee shop kadikoy”. But when you are doing research at scale, copying listings manually is not practical.

Pullbay’s Google Maps API lets you search places with a free-text query and returns structured fields such as place name, address, website, phone, rating, review count, categories, location, Google Maps URL, and FID.

### Endpoint

```
GET https://api.pullbay.com/google-maps/place/search

Authorization: Bearer YOUR_API_KEY
```

### Required Parameter

| Parameter | Type   | Required | Description                             |
| --------- | ------ | -------- | --------------------------------------- |
| `query`   | string | Yes      | Free-text search term, 1–200 characters |

### Optional Parameters

| Parameter      | Type    | Description                                       |
| -------------- | ------- | ------------------------------------------------- |
| `countryCode`  | string  | Country or region bias, 2–5 characters            |
| `languageCode` | string  | Language code for localized text, 2–10 characters |
| `latitude`     | string  | Latitude to center the search viewport            |
| `longitude`    | string  | Longitude to center the search viewport           |
| `zoom`         | string  | Map zoom level to use with latitude and longitude |
| `offset`       | string  | Offset-style pagination value                     |
| `maxItems`     | integer | One-shot bulk pull, 1–500 results                 |

`offset` and `maxItems` are mutually exclusive. Use one or the other in a request.

### Approach 1: Simple Keyword Search with `maxItems`

Use `maxItems` when you want a simple one-shot pull of places matching a keyword.

```python
import requests

API_KEY = "YOUR_API_KEY"

BASE_URL = "https://api.pullbay.com"

headers = {

"Authorization": f"Bearer {API_KEY}"

}

params = {

"query": "pizza near soho new york",

"countryCode": "us",

"languageCode": "en",

"maxItems": 20

}

response = requests.get(

f"{BASE_URL}/google-maps/place/search",

headers=headers,

params=params

)

response.raise_for_status()

data = response.json()

places = data.get("data", [])

credits = data.get("pricing", {}).get("creditsCharged")

print(f"Fetched {len(places)} places")

print(f"Credits charged: {credits}")

for place in places:

print(place.get("title"))

print(place.get("address"))

print(place.get("totalScore"), place.get("reviewsCount"))

print(place.get("url"))

print("---")
```

#### When to use this

Use this approach when you need up to 500 results in a single request and do not need to process results page by page.

### Approach 2: Location-Biased Search with Latitude, Longitude, and Zoom

If you want to bias search results toward a specific map viewport, pass latitude, longitude, and zoom.

```python
import requests

API_KEY = "YOUR_API_KEY"

BASE_URL = "https://api.pullbay.com"

headers = {

"Authorization": f"Bearer {API_KEY}"

}

params = {

"query": "coffee shops",

"countryCode": "us",

"languageCode": "en",

"latitude": "40.730610",

"longitude": "-73.935242",

"zoom": "13",

"maxItems": 50

}

response = requests.get(

f"{BASE_URL}/google-maps/place/search",

headers=headers,

params=params

)

response.raise_for_status()

data = response.json()

for place in data.get("data", []):

print({

"title": place.get("title"),

"address": place.get("address"),

"rating": place.get("totalScore"),

"reviews": place.get("reviewsCount"),

"fid": place.get("fid"),

"url": place.get("url")

})
```

#### When to use this

Use viewport parameters when your keyword is broad and you want results biased toward a specific area.

### Approach 3: Offset Pagination

Use `offset` when you want to process results in batches. The response includes `pagination.offset`. For this endpoint, increment the offset by the number of items received.

```python
import requests

API_KEY = "YOUR_API_KEY"

BASE_URL = "https://api.pullbay.com"

headers = {

"Authorization": f"Bearer {API_KEY}"

}

query = "restaurants in brooklyn"

offset = 0

all_places = []

while True:

params = {

"query": query,

"countryCode": "us",

"languageCode": "en",

"offset": str(offset)

}

response = requests.get(

f"{BASE_URL}/google-maps/place/search",

headers=headers,

params=params

)

response.raise_for_status()

data = response.json()

places = data.get("data", [])

if not places:

break

all_places.extend(places)

print(f"Fetched {len(places)} places at offset {offset}")

offset += len(places)

print(f"Total places fetched: {len(all_places)}")
```

#### When to use this

Use offset pagination when you want to process results incrementally instead of requesting a larger one-shot result set.

### Useful Fields in the Search Response

A place search result can include fields such as:

| Field                         | Description                                |
| ----------------------------- | ------------------------------------------ |
| `title`                       | Place name                                 |
| `url`                         | Google Maps URL for the place              |
| `address`                     | Full formatted address                     |
| `location.lat / location.lng` | Latitude and longitude                     |
| `website`                     | Place website URL                          |
| `phone`                       | Formatted phone number                     |
| `totalScore`                  | Average rating                             |
| `reviewsCount`                | Total number of reviews                    |
| `categories`                  | Place category labels                      |
| `openingHours`                | Opening hours per weekday                  |
| `fid`                         | Feature ID used for the Get Place endpoint |
| `placeId`                     | Google Place ID                            |

### Credit Considerations

Each request returns the exact credit charge in:

```
data["pricing"]["creditsCharged"]
```

Credit cost is dynamic. Do not hardcode a flat cost per endpoint or per result. Always read the value from the response.

### Troubleshooting

| Problem                     | Possible Cause                            | Fix                                                      |
| --------------------------- | ----------------------------------------- | -------------------------------------------------------- |
| 400 Bad request             | Missing or invalid parameter              | Check query, offset, maxItems, and geo parameter formats |
| 402 Insufficient credits    | Your account does not have enough credits | Check your Pullbay account credits                       |
| 404 Not found               | No matching place or review set found     | Try a different query or location bias                   |
| Empty result set            | Query may be too narrow                   | Broaden the keyword or remove viewport parameters        |
| Invalid pagination behavior | offset and maxItems used together         | Use only one pagination mode per request                 |

### Next Steps

After you can search Google Maps places by keyword, you can:

* Use each result’s `fid` to fetch full place details
* Use each result’s `url` to fetch reviews
* Store search results in a database for local SEO analysis
* Compare competitors by rating, review count, category, and location


# How-To Guides for App Store

The Pullbay App Store API gives you direct access to user reviews from any app on the App Store — by numeric ID or bundle ID, for any country, sorted by recency or helpfulness. These guides walk through the most common real-world tasks: exporting data, collecting full review history, catching negative feedback early, and scaling to multiple apps at once.

## What You Can Do

| Task                                       | When to use it                                                                                    |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| **Export reviews to CSV or a spreadsheet** | One-time or recurring data exports for reporting, sharing with stakeholders, or offline analysis  |
| **Collect complete review history**        | Building a full historical dataset from scratch, or catching up after a period without monitoring |
| **Monitor negative reviews in real time**  | Setting up alerts so your team can respond quickly to 1–2 star feedback                           |
| **Track reviews across multiple apps**     | Comparing your own app variants, localized versions, or monitoring competitors side by side       |

## Before You Start

All guides assume you have:

* A **Pullbay API key** — generate one from the [dashboard](https://dashboard.pullbay.com/)
* Your app's **numeric App Store ID** (8–12 digits, e.g. `389801252`) or **bundle ID** (e.g. `com.burbn.instagram`) — find it on the App Store product page or in App Store Connect
* Enough **credits** for the number of reviews you plan to fetch — each review returned costs 1 credit

## Guides in This Section

### Export App Store Reviews

Export reviews to a file, spreadsheet, or database in a few steps. Covers one-time exports (using `maxItems` for a bulk fetch) and recurring exports with deduplication so you don't re-process reviews you've already seen.

**Best for**: CSV exports, Google Sheets sync, seeding a data warehouse.

### Monitor Negative Reviews

Set up a recurring workflow that fetches recent reviews, filters to scores of 1 or 2, and sends alerts to Slack, email, or your support tool. Includes guidance on deduplication so each low-score review triggers exactly one alert.

**Best for**: Customer support triage, responding to critical feedback before it compounds.

### Track Reviews Across Multiple Apps

Fetch reviews for a list of apps in a single workflow — useful for comparing localized versions of your app, monitoring a portfolio of products, or running a weekly competitive analysis on rival apps.

**Best for**: Product teams managing more than one app, competitive intelligence, portfolio dashboards.

## Key API Concepts

### Two fetch modes — choose one per request

| Mode          | Parameter        | Returns                                            | Use when                                       |
| ------------- | ---------------- | -------------------------------------------------- | ---------------------------------------------- |
| **Bulk**      | `maxItems=1–500` | Up to 500 reviews in one response, no cursor       | You want all recent reviews at once            |
| **Paginated** | `page=1–10`      | \~50 reviews + `pagination.hasNextPage` + `cursor` | You need to process page by page or stop early |

`maxItems` and `page` cannot be used in the same request.

### App identifier — choose one per request

* `appId` — numeric App Store ID (e.g. `389801252`)
* `bundleId` — reverse-domain bundle ID (e.g. `com.burbn.instagram`)

`appId` and `bundleId` cannot be used together.

### Sort order

* `recent` — newest reviews first (default)
* `helpful` — most helpful votes first

### Response envelope

Every response wraps the data in the same structure:

```json
{
  "requestId": "3b8dcb68-...",
  "success": true,
  "data": [...],
  "pagination": { "hasNextPage": false, "cursor": null },
  "pricing": { "creditsCharged": 52 }
}
```

Always log `requestId` and `pricing.creditsCharged` — the first helps debug issues with support, the second lets you track credit consumption over time.

## Where to Go Next

* **New to the API?** Start with the Export guide — it covers the basics of authentication and reading the response before adding any automation.
* **Running a scheduled workflow?** The Monitor Negative Reviews guide includes a complete deduplication pattern you can adapt to any recurring use case.
* **Using n8n, Zapier, or Google Apps Script?** See the platform-specific integration guides for copy-paste workflow configurations.
* **Building a backend service?** See the Backend Integration Guide for language-specific client examples (Python, Node.js, PHP, Ruby) with retry logic, error handling, and credit tracking.


# What is Review Monitoring?

Every negative review is feedback. Every 1-star rating signals a bug. Every pattern in your reviews tells a story about your app's real-world performance. But if you're checking reviews manually — logging into the App Store, scrolling through pages, copy-pasting feedback into spreadsheets — you're missing insights and wasting time. App Store review monitoring automates this process so you can respond faster, fix bugs quicker, and understand your users better.

## What Is App Store Review Monitoring?

Review monitoring means continuously tracking, collecting, and analysing app reviews as they come in. With automated monitoring, you:

* **Collect reviews automatically** on a schedule (hourly, daily, etc.)
* **Track trends** in ratings, sentiment, and key topics
* **Detect problems early** — a spike in 1-star reviews, crash mentions, or "won't load" keywords
* **Alert the right team** — engineering for bugs, product for features, support for customer issues
* **Keep history** of all reviews for trend analysis and compliance

Manual review monitoring is reactive — you check the App Store when you remember, and by then you've missed days of feedback. Automated monitoring is proactive: new reviews flow into your systems on a schedule, and you can trigger alerts, dashboards, or integrations automatically.

## Why Review Monitoring Matters

### User Feedback Loop

Reviews are direct, unsolicited feedback from real users at scale. They tell you:

* What's working well (positive reviews highlight features users love)
* What's broken (negative reviews often describe bugs before crash reports arrive)
* What features users want (feature requests in review text)
* How different user segments experience your app (by country, version, device)

Without review monitoring, you're flying blind. With it, you're tapping into free, continuous user research.

### Early Bug Detection

Users often report bugs in reviews before they open support tickets. A sudden spike in 1-star or 2-star reviews with keywords like "crash", "freeze", or "won't load" signals a problem. With monitoring, you catch these issues hours before they hit your crash-reporting tool, so your engineering team can investigate and patch faster.

### Competitive Intelligence

Monitor your competitors' apps too (if they're public). Track their review trends, user complaints, and feature requests. When a competitor ships a bad release, you'll know immediately. When users praise a competitor feature, you can evaluate whether to build something similar.

### Reputation Management

A sudden influx of negative reviews can tank your app's overall rating. Review monitoring lets you:

* Respond quickly to problems (both with fixes and public developer replies)
* Spot fake reviews or spam and flag them
* Trace a rating drop to a specific version or incident and tell that story clearly

### Product Development

Aggregate review data to inform roadmap decisions. Which features do users want most? What friction points cause frustration? Review text, titles, and scores tell a data-driven story about priorities.

## Manual vs. Automated Monitoring

**Manual Monitoring — Slow & Error-Prone**

* Check the App Store 2–3 times per week
* See only the top or recent reviews (App Store UI is limited)
* Miss reviews posted while you weren't looking
* Copy-paste interesting reviews into a spreadsheet
* No historical data for trend analysis
* Can't trigger alerts or downstream actions

**Automated Monitoring — Fast & Complete**

* Poll the App Store on a schedule (every hour, every day, etc.)
* Fetch all available reviews (not just top-featured ones)
* Never miss a review
* Reviews flow automatically into your system (database, CRM, Slack, etc.)
* Build historical trends and compare periods
* Trigger alerts, emails, or webhooks based on rules

Automated monitoring scales from one app to dozens. Once set up, it runs hands-free.

## What Data Does Pullbay Provide?

When you fetch reviews using Pullbay's App Store API, each review object contains:

| Field      | Example                               | Use Case                                               |
| ---------- | ------------------------------------- | ------------------------------------------------------ |
| `id`       | `"13997548250"`                       | Deduplication; primary key in your database            |
| `score`    | `1`, `2`, `3`, `4`, `5`               | Filter by star count; track average score trends       |
| `title`    | `"Best app ever!"`                    | Sentiment analysis; identify key topics                |
| `text`     | `"Love the dark mode. Please add..."` | Parse feature requests and bug reports                 |
| `userName` | `"JohnDoe123"`                        | Track repeat reviewers; spot fake reviews              |
| `date`     | `"2026-04-25T19:57:33-07:00"`         | Identify when issues occurred; correlate with releases |
| `version`  | `"426.0.0"`                           | Link feedback to specific releases; detect regressions |
| `country`  | `"US"`                                | Filter by region; spot localisation issues             |
| `url`      | `"https://itunes.apple.com/..."`      | Link to the review for developer reply                 |

## How to Set Up Automated Review Monitoring

{% stepper %}
{% step %}

## Get Your Pullbay API Key

Sign up at [pullbay.com](https://pullbay.com/) and copy your API key from the dashboard under **API Keys**.
{% endstep %}

{% step %}

## Create a Scheduled Polling Script

This Python script fetches up to 500 reviews per country and saves them locally:

```python
import requests
import json
from datetime import datetime

API_KEY  = "YOUR_API_KEY"
APP_ID   = "284882215"                          # Replace with your app's numeric ID
COUNTRIES = ["us", "gb", "de", "jp", "au"]     # Storefronts to monitor

def fetch_reviews(country):
    response = requests.get(
        "https://api.pullbay.com/appstore/reviews",
        params={
            "appId":    APP_ID,
            "country":  country,
            "sort":     "recent",
            "maxItems": 500,
        },
        headers={"Authorization": f"Bearer {API_KEY}"},
        timeout=120,
    )
    if response.status_code == 200:
        return response.json()
    print(f"Error fetching {country}: {response.status_code}")
    return None

def monitor_reviews():
    timestamp   = datetime.utcnow().isoformat()
    all_reviews = []

    for country in COUNTRIES:
        print(f"Fetching reviews for {country}...")
        body = fetch_reviews(country)

        if body:
            all_reviews.extend(body["data"])
            print(f"  {len(body['data'])} reviews — requestId: {body['requestId']}, credits: {body['pricing']['creditsCharged']}")

    with open(f"reviews_{timestamp}.json", "w") as f:
        json.dump(all_reviews, f, indent=2)

    print(f"\nTotal reviews collected: {len(all_reviews)}")
    return all_reviews

if __name__ == "__main__":
    monitor_reviews()
```

{% endstep %}

{% step %}

## Schedule the Script

{% tabs %}
{% tab title="Linux / macOS (cron)" %}

```bash
# Every hour
0 * * * * /usr/bin/python3 /path/to/monitor_reviews.py

# Every 6 hours
0 */6 * * * /usr/bin/python3 /path/to/monitor_reviews.py
```

{% endtab %}

{% tab title="Windows" %}
Create a Task Scheduler task that runs `python C:\path\to\monitor_reviews.py` at your desired interval.
{% endtab %}

{% tab title="Cloud" %}
Deploy as a serverless function (AWS Lambda, Google Cloud Functions) and trigger via EventBridge or Cloud Scheduler.
{% endtab %}
{% endtabs %}
{% endstep %}
{% endstepper %}

## What to Do With Your Review Data

### Alert on Low-Score Reviews

Flag newly published 1- or 2-star reviews so your team can respond quickly:

```python
def alert_on_low_scores(reviews, threshold=2):
    low = [r for r in reviews if r["score"] <= threshold]

    if low:
        print(f"ALERT: {len(low)} review(s) with score ≤ {threshold}:")
        for review in low:
            print(f"  {review['score']}⭐  {review['title']}")
            print(f"  {review['text'][:150]}...")
```

### Track Score Trends

Compute average score over time to detect regressions:

```python
def track_score_trend(reviews):
    if not reviews:
        return

    avg   = sum(r["score"] for r in reviews) / len(reviews)
    dist  = {i: sum(1 for r in reviews if r["score"] == i) for i in range(1, 6)}

    print(f"Average score: {avg:.2f}⭐")
    print(f"Distribution: " + "  ".join(f"{i}⭐={dist[i]}" for i in range(1, 6)))

    if avg < 3.5:
        print("WARNING: average score below 3.5")
```

### Send Alerts to Slack

Notify your team in real time when critical reviews appear:

```python
import requests

def send_to_slack(reviews, webhook_url, threshold=2):
    low = [r for r in reviews if r["score"] <= threshold]
    if not low:
        return

    message = f"*Review Alert:* {len(low)} new low-score review(s)\n"
    for review in low[:5]:
        message += (
            f"\n{review['score']}⭐  *{review['title']}*"
            f"\n_{review['text'][:100]}..._"
            f"\n{review['userName']} — {review['country']} — v{review['version']}\n"
        )

    requests.post(webhook_url, json={"text": message})

SLACK_WEBHOOK = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
send_to_slack(reviews, SLACK_WEBHOOK)
```

### Store in a Database

Persist reviews for historical trend analysis:

```python
def store_reviews(reviews, db_connection, app_id):
    cursor = db_connection.cursor()

    for review in reviews:
        cursor.execute("""
            INSERT OR IGNORE INTO app_reviews
              (review_id, app_id, score, title, text, user_name, date, version, country)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
        """, (
            review["id"],
            app_id,               # From your config, not the review object
            review["score"],
            review["title"],
            review["text"],
            review["userName"],
            review["date"],
            review["version"],
            review["country"],
        ))

    db_connection.commit()
```

Build dashboards on top of this table to visualise score trends, segment by region or version, and spot patterns over time.

### Extract Keywords from Review Text

Find recurring topics in review titles and text:

```python
def extract_keywords(reviews, top_n=10):
    counts = {}

    for review in reviews:
        words = (review["title"] + " " + review["text"]).lower().split()
        for word in words:
            if len(word) > 4:          # Skip short words
                counts[word] = counts.get(word, 0) + 1

    top = sorted(counts.items(), key=lambda x: x[1], reverse=True)[:top_n]
    print("Top keywords:")
    for word, count in top:
        print(f"  {word}: {count}")
```

## Review Monitoring Use Cases

| Use Case                  | How It Helps                                                                 | Implementation                                                            |
| ------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| **Bug Tracking**          | Catch crashes and glitches reported in reviews before support tickets arrive | Alert on `score ≤ 2` + keywords like "crash", "freeze", "won't load"      |
| **Feature Feedback**      | Identify what features users want most                                       | Aggregate keyword frequency in review text                                |
| **Competitive Analysis**  | Monitor competitor apps and their issues                                     | Run the same monitoring pipeline on competitor `appId`s                   |
| **Release Health**        | Check if a new version caused a score drop                                   | Filter by `version` field; compare average score before/after release     |
| **Customer Support**      | Prioritise responses to high-impact reviews                                  | Alert team to newest low-score reviews; link to `url` for developer reply |
| **Reputation Management** | Respond quickly to maintain app rating                                       | Dashboard showing score trend and unanswered low-score reviews            |
| **Localisation Quality**  | Check feedback from specific regions                                         | Filter by `country` field; spot region-specific issues                    |

## Common Monitoring Patterns

### Real-Time Alerts

Fetch reviews every hour. Alert on any score ≤ 2 or reviews containing keywords like "crash" or "bug". Best for catching critical issues fast.

### Weekly Digest

Collect reviews for 7 days, then send a summary: total reviews, average score, top complaints, and positive highlights. Good for product and leadership updates.

### Trend Detection

Compare this week's average score to last week's. If it drops more than 0.5 stars, investigate — check version changes, marketing campaigns, or competitor activity.

### Competitive Benchmarking

Monitor your top competitors' apps alongside your own. Track relative scores over time and note when a competitor gains or loses momentum.

## Key Takeaways

* **Manual review checking is reactive** — automated monitoring is proactive
* **Review monitoring lets you** detect bugs early, understand user needs, manage reputation, and inform product decisions
* **Pullbay's API** provides structured review data: `score`, `text`, `userName`, `date`, `version`, `country`, and more
* **Set up automated polling** with a Python script and a scheduler (cron, Lambda, etc.)
* **Act on the data** — alert your team, track score trends, feed into Slack, store in your database, and build dashboards
* **The faster you respond to reviews**, the faster you fix issues and retain users


# App Store Review API Guide

When you're building or maintaining an app, understanding user feedback is critical. But manually checking App Store reviews doesn't scale. An app store review API lets you pull review data automatically, in structured format, so you can build tools, track trends, and respond to feedback at scale. This guide covers everything you need to know about Pullbay's App Store Review API and how to use it in your workflows.

***

## What Is an App Store Review API?

An app store review API is a programmatic interface that lets you retrieve user reviews, ratings, and metadata directly from app stores like Apple's App Store. Instead of logging in manually and scrolling through reviews, an API lets you:

* **Fetch reviews programmatically** using code
* **Retrieve structured data** — score, text, author, date, country, version, and more
* **Automate monitoring** with scheduled requests
* **Scale to hundreds of reviews** without manual effort
* **Integrate with your own tools** — dashboards, alerts, CRMs, databases

### Why You Need an App Store Review API

Manual review monitoring is slow and error-prone. An API solves three core problems:

1. **Speed** — get fresh review data in seconds, not hours
2. **Scale** — fetch up to 500 reviews automatically without clicking through pages
3. **Integration** — feed review data directly into your analytics, CRM, or alerting systems

Whether you're tracking competitor apps, monitoring your own reputation, or analysing user feedback for feature development, an API approach is faster and more reliable than manual tools.

***

## How Pullbay's App Store Review API Works

Pullbay's API retrieves reviews from Apple's App Store in three steps:

{% stepper %}
{% step %}

## You send a request

Specify the app you want (by numeric App Store ID or bundle ID) and any options (country, sort order, number of reviews).
{% endstep %}

{% step %}

## Pullbay fetches the data

Pullbay calls Apple's review feed, handles pagination, deduplication, and normalisation, and returns a clean response.
{% endstep %}

{% step %}

## You receive structured JSON

Reviews arrive as clean, normalised JSON with consistent field names, data types, and a standard response envelope.

You don't need Apple Developer credentials or App Store tokens — just your Pullbay API key and the app's ID.
{% endstep %}
{% endstepper %}

***

## Key Features

### Two Fetch Modes

The `/api/appstore/reviews` endpoint supports two mutually exclusive modes:

| Mode            | Parameter             | Behaviour                                                                            |
| --------------- | --------------------- | ------------------------------------------------------------------------------------ |
| **Bulk**        | `maxItems=1–500`      | Pullbay paginates Apple's feed internally and returns all results in one response    |
| **Single page** | `page=1–10` (or omit) | Returns \~50 reviews for that page, with `pagination.hasNextPage` so you can iterate |

`maxItems` and `page` cannot be used together in the same request.

### App Identifier Options

Each request requires exactly one identifier — `appId` (numeric) or `bundleId` (reverse-domain). They cannot be used together.

### Normalised Data

Review text, titles, and metadata are cleaned for consistency. Field names and types are stable across all responses.

### Structured Error Responses

Errors follow the same envelope as success responses, with a clear `error.code` and `message`.

***

## How to Fetch App Store Reviews: Practical Examples

You'll need:

* A Pullbay API key — get one free at [pullbay.com](https://pullbay.com/)
* An App Store numeric app ID (e.g. `284882215` for X/Twitter) or bundle ID (e.g. `com.atebits.Tweetie2`)

### cURL

```bash
curl "https://api.pullbay.com/appstore/reviews?appId=284882215&country=us&sort=recent&maxItems=50" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Python

```python
import requests

API_KEY = "YOUR_API_KEY"

response = requests.get(
    "https://api.pullbay.com/appstore/reviews",
    params={
        "appId":    "284882215",   # X/Twitter
        "country":  "us",
        "sort":     "recent",      # "recent" or "helpful"
        "maxItems": 50,
    },
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=30,
)

body = response.json()
print(f"requestId: {body['requestId']}")
print(f"Retrieved {len(body['data'])} reviews — {body['pricing']['creditsCharged']} credits")

for review in body["data"]:
    print(f"{review['score']}⭐  {review['title']}")
    print(f"  {review['text'][:100]}...")
```

### Bulk Fetch — All Reviews in One Call

Use `maxItems=500` to get up to 500 reviews in a single request. Pullbay paginates Apple's feed internally and returns everything at once:

```python
import requests

API_KEY = "YOUR_API_KEY"

response = requests.get(
    "https://api.pullbay.com/appstore/reviews",
    params={
        "appId":    "284882215",
        "country":  "us",
        "maxItems": 500,
    },
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=120,   # Allow time for internal pagination
)

body = response.json()
print(f"Total reviews: {len(body['data'])}")
print(f"Credits charged: {body['pricing']['creditsCharged']}")
print(f"requestId: {body['requestId']}")
```

### Page-by-Page Iteration

Use `page` when you want to process reviews incrementally or stop early:

```python
import requests

API_KEY = "YOUR_API_KEY"
all_reviews = []

for page in range(1, 11):   # Apple's feed caps at 10 pages
    response = requests.get(
    "https://api.pullbay.com/appstore/reviews",
        params={"appId": "284882215", "country": "us", "page": page},
        headers={"Authorization": f"Bearer {API_KEY}"},
        timeout=30,
    )
    body = response.json()
    all_reviews.extend(body["data"])

    if not body["pagination"]["hasNextPage"]:
        break

print(f"Collected {len(all_reviews)} reviews")
```

***

## Understanding the API Response

Every Pullbay response follows the same envelope:

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "13997548250",
      "date": "2026-04-25T19:57:33-07:00",
      "userName": "JohnDoe123",
      "userUrl": "https://itunes.apple.com/us/reviews/id1632846422",
      "score": 5,
      "title": "Best app for staying connected",
      "text": "Love the new features in the latest update!",
      "version": "10.2.1",
      "url": "https://itunes.apple.com/us/review?id=284882215&type=Purple%20Software",
      "country": "US"
    }
  ],
  "pagination": {
    "page": 1,
    "hasNextPage": true,
    "cursor": null,
    "offset": null
  },
  "pricing": {
    "creditsCharged": 52
  }
}
```

### Top-Level Fields

| Field                    | Description                                                                                                     |
| ------------------------ | --------------------------------------------------------------------------------------------------------------- |
| `requestId`              | Unique request identifier — log this for every call; it is the key piece of information when contacting support |
| `success`                | `true` on success, `false` on error                                                                             |
| `data`                   | Array of review objects                                                                                         |
| `pagination`             | Page state — only meaningful when using `page` mode; fields are `null` in bulk (`maxItems`) mode                |
| `pricing.creditsCharged` | Credits consumed by this request                                                                                |

### Review Object Fields

| Field      | Type        | Description                                                     |
| ---------- | ----------- | --------------------------------------------------------------- |
| `id`       | string      | Unique review identifier — use as primary key for deduplication |
| `score`    | integer 1–5 | Star rating                                                     |
| `title`    | string      | Review headline                                                 |
| `text`     | string      | Full review body                                                |
| `userName` | string      | Reviewer's App Store display name                               |
| `userUrl`  | string      | Link to reviewer's profile                                      |
| `date`     | string      | ISO 8601 datetime when the review was posted                    |
| `version`  | string      | App version the reviewer had installed                          |
| `url`      | string      | Direct link to the review (for developer reply)                 |
| `country`  | string      | 2-letter country code of the storefront                         |

### Pagination Fields

`pagination` is only meaningful in `page` mode. In `maxItems` (bulk) mode, all fields are `null`.

| Field         | Description                                                     |
| ------------- | --------------------------------------------------------------- |
| `hasNextPage` | `true` if more pages exist — use to control your iteration loop |
| `page`        | The page number returned                                        |

### Error Responses

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 400,
  "message": "Validation failed",
  "success": false,
  "error": { "code": "VALIDATION_ERROR" }
}
```

| HTTP Status | Error Code                    | Cause                                 |
| ----------- | ----------------------------- | ------------------------------------- |
| `400`       | `VALIDATION_ERROR`            | Invalid parameter name or value       |
| `401`       | `AUTHENTICATION_UNAUTHORIZED` | Invalid or missing API key            |
| `402`       | `TEAM_INSUFFICIENT_CREDITS`   | Credit balance is 0                   |
| `429`       | `RATE_LIMIT_EXCEEDED`         | Request rate exceeded for your plan   |
| `5xx`       | —                             | Server-side error; retry with backoff |

***

## Rate Limits

Rate limits are enforced per API key on a per-second and per-minute basis. Exact limits depend on your plan — check the dashboard under **Account → Plan**. Credits are charged per review returned, not per request.

Failed requests (any non-2xx response) do not consume credits.

***

## Frequently Asked Questions

<details>

<summary>Does Pullbay require App Store credentials?</summary>

No. You don't need an Apple Developer account or App Store credentials. Provide the public app ID (visible in the App Store URL) or bundle ID, and Pullbay handles the rest.

</details>

<details>

<summary>How fresh is the review data?</summary>

Data is fetched from Apple's review feed on demand when you make a request. Apple's feed has a natural lag of 1–24 hours from when a review is published to when it appears. This is an Apple limitation, not a Pullbay limitation. For monitoring, polling every 1–6 hours is sufficient for most use cases.

</details>

<details>

<summary>Can I get reviews for any country?</summary>

Yes. Use the `country` parameter with a 2-letter ISO 3166-1 alpha-2 code (e.g. `us`, `gb`, `jp`). The App Store has regional review feeds; Pullbay supports all active storefronts.

</details>

<details>

<summary>Can I filter reviews by date or rating?</summary>

No — the endpoint does not support `since`, `until`, or rating filter parameters. Fetch reviews and apply filters in your own code:

```python
# Filter to last 7 days
from datetime import datetime, timezone
cutoff  = datetime.now(timezone.utc).timestamp() - 7 * 86400
recent  = [r for r in body["data"] if datetime.fromisoformat(r["date"]).timestamp() > cutoff]

# Filter to low scores
low = [r for r in body["data"] if r["score"] <= 2]
```

</details>

<details>

<summary>What is the maximum number of reviews I can fetch?</summary>

Apple's review feed is limited to 10 pages of \~50 reviews each — a maximum of **500 reviews** per app per country storefront. Use `maxItems=500` to receive all of them in a single response.

</details>

<details>

<summary>What happens if a request fails?</summary>

Failed requests don't consume credits — you are only charged for successful `2xx` responses. If you receive a `5xx` error, retry with exponential backoff.

</details>

<details>

<summary>Can I use `bundleId` instead of `appId`?</summary>

Yes. `bundleId` (e.g. `com.atebits.Tweetie2`) is a valid alternative identifier. Pass exactly one of `appId` or `bundleId` — not both.

</details>

***

## Next Steps

1. **Get your API key** — sign up at [pullbay.com](https://pullbay.com/) and create a key from the dashboard
2. **Run your first request** — copy the cURL example above and replace `YOUR_API_KEY` with your actual key
3. **Set up monitoring** — see the Review Monitoring Guide for scheduling patterns, alerting, and database storage
4. **Build integrations** — see the n8n, Google Sheets, and Backend Integration guides for workflow examples

For questions or to report issues, contact <support@pullbay.com> and include the `requestId` from the error response.

***

## Key Takeaways

* The endpoint is `GET https://api.pullbay.com/appstore/reviews`
* Use `appId` (numeric) or `bundleId` (reverse-domain) — exactly one, never both
* Use `maxItems=1–500` for a bulk single-call fetch; use `page=1–10` to iterate page by page — never both in the same request
* Sort options are `recent` (default) and `helpful` only
* Review fields: `score` (not `rating`), `text` (not `content`), `userName` (not `author`)
* Credits are reported in `pricing.creditsCharged` — log it alongside `requestId` for every call
* Check `pagination.hasNextPage` (not `has_more`) when paginating
* Failed requests do not consume credits
* Maximum 500 reviews per app per country (Apple's feed limit)


# App Store Data Guide

When you fetch app reviews with Pullbay, you get rich structured data for each review. This guide explains every field in the response, what each one means, how to use it, and real-world examples. Whether you're building a dashboard, analysing sentiment, or filtering reviews by region, understanding your data is the foundation of smart review monitoring.

## The Complete App Store Review Object

Here's a full example response with every field explained:

```json
{
  "requestId": "3b8dcb68-1f8c-4a7b-b9e7-63a7b9986f24",
  "status": 200,
  "message": "OK",
  "success": true,
  "data": [
    {
      "id": "13997548250",
      "date": "2026-01-15T14:32:00-07:00",
      "userName": "ProductiveUser42",
      "userUrl": "https://itunes.apple.com/us/reviews/id1632846422",
      "score": 4,
      "title": "Great app but needs dark mode",
      "text": "I've been using this app for 2 years. It's excellent for productivity, but I really wish there was a dark mode option. My eyes hurt after using it in the evening. Otherwise, perfect!",
      "version": "10.2.1",
      "url": "https://itunes.apple.com/us/review?id=284882215&type=Purple%20Software",
      "country": "US"
    },
    {
      "id": "13997548251",
      "date": "2026-01-16T09:15:00-07:00",
      "userName": "FrustratedUser88",
      "userUrl": "https://itunes.apple.com/us/reviews/id9988776655",
      "score": 1,
      "title": "Crashes on startup",
      "text": "Updated to the latest version yesterday and now it won't even open. Just crashes immediately. Please fix ASAP!",
      "version": "10.3.0",
      "url": "https://itunes.apple.com/us/review?id=284882215&type=Purple%20Software",
      "country": "US"
    },
    {
      "id": "13997548252",
      "date": "2026-01-14T16:45:00-07:00",
      "userName": "BudgetConsciousUser",
      "userUrl": "https://itunes.apple.com/us/reviews/id1122334455",
      "score": 3,
      "title": "Good but expensive",
      "text": "The app works well, but the subscription cost is way too high compared to competitors.",
      "version": "10.2.0",
      "url": "https://itunes.apple.com/us/review?id=284882215&type=Purple%20Software",
      "country": "US"
    }
  ],
  "pagination": {
    "page": 1,
    "hasNextPage": true,
    "cursor": null,
    "offset": null
  },
  "pricing": {
    "creditsCharged": 3
  }
}
```

## Field Reference and Use Cases

### Envelope Fields

#### `requestId` (string)

Unique identifier for the API request itself — not the review. Log this for every call.

* **Use cases**: Debugging; include in support tickets so issues can be traced in logs

#### `pricing.creditsCharged` (integer)

Credits consumed by this request. Equals the number of reviews returned × the per-item cost for your task.

* **Use cases**: Credit monitoring; logging spend per call

#### `pagination.hasNextPage` (boolean)

Whether more pages exist. Only meaningful when using `page` mode — always `false` in `maxItems` (bulk) mode.

* **Use cases**: Loop control in page-by-page iteration

#### `pagination.page` (integer)

The page number returned.

### Review Object Fields

#### `id` (string)

Unique identifier for this review. Stable — never changes for the lifetime of the review.

* **Data type**: String (numeric string, e.g. `"13997548250"`)
* **Use cases**:
  * Primary key when storing reviews in your database
  * Deduplication: skip inserting a review if you've already seen this ID
  * Cross-referencing: link reviews to internal tickets, alerts, or threads

***

#### `score` (integer)

Star rating from 1 (lowest) to 5 (highest).

* **Data type**: Integer, values 1–5
* **Examples**: `1`, `3`, `5`
* **Use cases**:
  * Average rating: `sum(r["score"] for r in reviews) / len(reviews)`
  * Distribution: count reviews per star level
  * Sentiment filter: `[r for r in reviews if r["score"] <= 2]` for negative reviews
  * Trend detection: compare average score this week vs. last week
  * Release regression: if average score drops after a version release, investigate

#### `title` (string)

The review headline — typically 2–20 words, often summarises the main point.

* **Data type**: String
* **Examples**: `"Great app but needs dark mode"`, `"Crashes on startup"`
* **Use cases**:
  * First-pass triage: the title usually communicates the core issue before you read the full text
  * Keyword extraction: parse titles to identify recurring topics ("dark mode", "crash", "expensive")
  * Sentiment analysis: apply NLP to detect positive or negative language
  * Search: index titles to find reviews about specific topics

#### `text` (string)

The full review body — typically 1–500+ words with detailed feedback, bug descriptions, feature requests, and context.

* **Data type**: String (variable length, can span multiple paragraphs)
* **Example**: `"I've been using this app for 2 years. It's excellent for productivity, but I really wish..."`
* **Use cases**:
  * Bug detection: search for keywords like "crash", "freeze", "won't load", "error"
  * Feature request extraction: parse text for "please add", "would love", "feature request"
  * Customer support: read `text` to understand the user's exact problem in detail
  * Sentiment analysis and topic modelling: train ML models or use an LLM on review text

#### `userName` (string)

The App Store display name of the reviewer (pseudonymous — not a real name).

* **Data type**: String
* **Examples**: `"ProductiveUser42"`, `"FrustratedUser88"`
* **Use cases**:
  * Repeat reviewer detection: track if the same user reviews multiple times (may indicate a power user or chronic complainer)
  * Fake review detection: flag accounts posting many low-score reviews in rapid succession
  * Community building: identify engaged power users

#### `userUrl` (string)

Link to the reviewer's App Store profile page.

* **Data type**: String (URL)
* **Example**: `"https://itunes.apple.com/us/reviews/id1632846422"`
* **Use cases**: Direct link to the reviewer's profile for manual investigation

#### `date` (string, ISO 8601)

When the review was posted.

* **Data type**: String (ISO 8601 datetime with timezone offset)
* **Example**: `"2026-01-15T14:32:00-07:00"`
* **Use cases**:
  * Timeline correlation: link review spikes to app releases, marketing campaigns, or outages
  * Recency filtering: focus on reviews from the past 7 or 30 days
  * Trend analysis: group reviews by week, month, or release cycle
  * Regression detection: if average score drops on a specific date, check what changed

#### `version` (string)

The app version the reviewer had installed when they wrote the review.

* **Data type**: String (semantic version: `major.minor.patch`)
* **Examples**: `"10.2.1"`, `"10.3.0"`, `"9.1.2"`
* **Use cases**:
  * Release correlation: if reviews for `v10.3.0` have a lower average score, that version likely introduced a problem
  * Regression detection: compare `v10.2.1` vs. `v10.3.0` review sets to pinpoint what broke
  * Support prioritisation: identify whether a complaint applies to a still-supported version
  * Adoption tracking: see which versions your reviewers are running

#### `url` (string)

Direct link to the review on the App Store.

* **Data type**: String (URL)
* **Example**: `"https://itunes.apple.com/us/review?id=284882215&type=Purple%20Software"`
* **Use cases**: Open the review directly in a browser to write a developer reply

#### `country` (string)

Two-letter ISO 3166-1 alpha-2 country code for the storefront where the review was posted. Uppercase in the response.

* **Data type**: String (2-letter uppercase, e.g. `"US"`, `"GB"`)
* **Examples**: `"US"`, `"GB"`, `"DE"`, `"JP"`, `"AU"`, `"FR"`, `"IN"`
* **Use cases**:
  * Regional analysis: compare feedback between the US, UK, Germany, Japan
  * Localisation QA: spot region-specific bugs or translation issues
  * Launch monitoring: track reviews in new markets you've just expanded to
  * Market research: understand how different regions perceive your app

> **Note:** The `country` field in the response is uppercase (`"US"`). The `country` request parameter is lowercase (`"us"`). Both represent the same storefront.

## How to Filter and Query Reviews

### Filter by Score

```python
# Negative reviews (1–2 stars)
low = [r for r in reviews if r["score"] <= 2]
print(f"Negative reviews: {len(low)}")

# Positive reviews (4–5 stars)
high = [r for r in reviews if r["score"] >= 4]
print(f"Positive reviews: {len(high)}")
```

### Filter by Country (at Request Level)

Country filtering is applied at the API level via the `country` query parameter — each request returns reviews from one storefront:

```bash
# US App Store (default)
curl "https://api.pullbay.com/appstore/reviews?appId=284882215&country=us" \
  -H "Authorization: Bearer YOUR_API_KEY"

# Japan App Store
curl "https://api.pullbay.com/appstore/reviews?appId=284882215&country=jp" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```python
import requests

countries   = ["us", "gb", "de", "fr"]
by_country  = {}

for country in countries:
    body = requests.get(
        "https://api.pullbay.com/appstore/reviews",
        params={"appId": "284882215", "country": country, "maxItems": 100},
        headers={"Authorization": f"Bearer {API_KEY}"},
        timeout=120,
    ).json()
    by_country[country] = body["data"]
    print(f"{country}: {len(body['data'])} reviews — {body['pricing']['creditsCharged']} credits")
```

### Filter by Version

```python
# Reviews for a specific version
v10_3_0 = [r for r in reviews if r["version"] == "10.3.0"]
avg      = sum(r["score"] for r in v10_3_0) / len(v10_3_0) if v10_3_0 else 0
print(f"v10.3.0 average score: {avg:.2f}")

# Compare two versions
def compare_versions(reviews, v1, v2):
    r1   = [r for r in reviews if r["version"] == v1]
    r2   = [r for r in reviews if r["version"] == v2]
    avg1 = sum(r["score"] for r in r1) / len(r1) if r1 else 0
    avg2 = sum(r["score"] for r in r2) / len(r2) if r2 else 0
    print(f"{v1}: {avg1:.2f}⭐ ({len(r1)} reviews)")
    print(f"{v2}: {avg2:.2f}⭐ ({len(r2)} reviews)")
    if avg2 < avg1 - 0.5:
        print(f"Warning: {v2} is scoring significantly lower than {v1}")
```

### Filter by Keyword

```python
def find_keyword(reviews, keyword):
    kw = keyword.lower()
    return [
        r for r in reviews
        if kw in r["title"].lower() or kw in r["text"].lower()
    ]

crash_reviews    = find_keyword(reviews, "crash")
dark_mode        = find_keyword(reviews, "dark mode")
pricing_mentions = find_keyword(reviews, "expensive")

print(f"Crash-related: {len(crash_reviews)}")
print(f"Dark mode requests: {len(dark_mode)}")
print(f"Pricing complaints: {len(pricing_mentions)}")
```

### Filter by Date

```python
from datetime import datetime, timezone, timedelta

cutoff  = datetime.now(timezone.utc) - timedelta(days=7)
recent  = [
    r for r in reviews
    if datetime.fromisoformat(r["date"]) > cutoff
]
print(f"Reviews in last 7 days: {len(recent)}")
```

## Pagination

### Bulk Fetch — `maxItems`

Use `maxItems` when you want all available reviews in a single call. Pullbay paginates Apple's feed internally and returns up to 500 reviews at once. No `pagination` fields are populated in the response.

```python
import requests

body = requests.get(
    "https://api.pullbay.com/appstore/reviews",
    params={"appId": "284882215", "country": "us", "maxItems": 500},
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=120,
).json()

reviews = body["data"]
print(f"Total: {len(reviews)}, Credits: {body['pricing']['creditsCharged']}")
```

### Page-by-Page — `page`

Use `page` when you want to process results incrementally or stop early. Each page returns \~50 reviews.

```python
import requests

all_reviews = []
page        = 1

while True:
    body = requests.get(
        "https://api.pullbay.com/appstore/reviews",
        params={"appId": "284882215", "country": "us", "page": page},
        headers={"Authorization": f"Bearer {API_KEY}"},
        timeout=30,
    ).json()

    all_reviews.extend(body["data"])
    print(f"Page {page}: {len(body['data'])} reviews — hasNextPage: {body['pagination']['hasNextPage']}")

    if not body["pagination"]["hasNextPage"]:
        break

    page += 1
    if page > 10:
        break  # Apple's feed is hard-capped at 10 pages

print(f"Total collected: {len(all_reviews)}")
```

`maxItems` and `page` cannot be used in the same request — choose one.

## Data Freshness

Data is fetched from Apple's review feed on demand. Apple's feed has a natural lag of 1–24 hours from when a review is published to when it appears. This is an Apple limitation, not a Pullbay limitation.

For most use cases — bug detection, trend monitoring, competitive analysis — polling every 1–6 hours is sufficient. For time-sensitive alerting (e.g. immediately catching a post-release spike), poll hourly and filter by `date` to process only reviews newer than your last run.

## Real-World Use Cases by Field

| Field      | Use Case                                         | Example                                                  |
| ---------- | ------------------------------------------------ | -------------------------------------------------------- |
| `id`       | Deduplication; link to internal tickets          | Store in DB with `INSERT OR IGNORE`                      |
| `score`    | Average score; detect rating drops               | "Average dropped to 3.2⭐ after v10.3.0"                  |
| `title`    | Quick triage; keyword extraction                 | "Crashes on startup" → alert engineering                 |
| `text`     | Detailed analysis; sentiment; feature extraction | Find all dark mode requests; parse bug descriptions      |
| `userName` | Repeat reviewer detection; fake review flags     | "User X posted 10 low-score reviews this week"           |
| `date`     | Timeline correlation                             | "Score spike on Jan 15 correlates with v10.2.1 release"  |
| `version`  | Regression detection; release health             | "v10.3.0 avg 2.1⭐ vs v10.2.1 avg 4.3⭐"                   |
| `country`  | Regional bugs; market research                   | "Germany has higher crash reports — localisation issue?" |
| `url`      | Direct link for developer reply                  | Open review in browser to write a response               |

## Common Questions

<details>

<summary>Can I request reviews for a specific date range?</summary>

No — the endpoint does not support `since` or `until` parameters. Fetch reviews and filter by `date` in your own code (see the date filter example above).

</details>

<details>

<summary>What if a review is edited or deleted?</summary>

When a user edits a review, the updated text appears on the next fetch. When a review is deleted, it no longer appears in the response. Pullbay does not track edit history or deletions — it always returns the current state from Apple's feed.

</details>

<details>

<summary>Can I export all historical reviews?</summary>

Yes. Use `maxItems=500` to fetch all reviews currently available in Apple's feed for an app. The maximum is 500 reviews per app per country storefront (Apple's RSS feed depth limit). Store results in your own database for long-term history and trend analysis.

</details>

<details>

<summary>How do I know how many credits a request consumed?</summary>

Every response includes `pricing.creditsCharged`. Log it alongside `requestId` for every call:

```python
body = response.json()
print(f"requestId: {body['requestId']}, credits: {body['pricing']['creditsCharged']}")
```

</details>

## Key Takeaways

* **Every field serves a purpose**: `score` and `text` tell you what the issue is; `version` links it to a code release; `country` reveals regional patterns; `date` enables timeline correlation
* **Use `score`**, not `rating` — use `text`, not `content` — use `userName`, not `author`
* **Use `id` as your primary key** for deduplication when storing reviews in a database
* **Pagination**: use `maxItems=500` for a single bulk call; use `page=1–10` to iterate page by page — never both together
* **Check `pagination.hasNextPage`** (not `has_more`) to control your iteration loop
* **Credits are reported in `pricing.creditsCharged`** — log it alongside `requestId` for every call
* **Date filtering is client-side** — fetch reviews and filter by `date` in your code; the API has no `since`/`until` parameters
* **Store reviews locally** for long-term trend analysis; Apple's feed only exposes the most recent 500 reviews per app per country


