# GlobeData LinkedIn Scraper API

> **Browsable version:** the same reference with copy-paste cURL / Python / Node.js examples, use-case
> guidance and full recipes lives at **<https://globedata.io/scraper-api-docs.html>**.

Programmatic access to LinkedIn **profile** and **company** data. Give us a LinkedIn handle (or full URL) and we return the structured record. This is the same surface area you see in the **Scraper** tab in the dashboard, exposed as a versioned REST API.

- **Base URL:** `https://api.globedata.io/api/v1/scraper`
- **Auth:** API key in the `Authorization` header
- **Format:** JSON request/response, `Content-Type: application/json`
- **Rate limit:** 60 requests / minute / key
- **Billing:** **per request** — every scrape costs exactly **1 request** from your wallet (see [§5 Billing](#5-billing))

---

## 1. Authentication

Every request must include a Bearer token containing your API key:

```
Authorization: Bearer gd_<32 alphanumeric chars>
```

Generate, list, and revoke keys from the **Scraper → API Keys** panel in the dashboard. The full key is shown **once** at creation; we store only a SHA-256 hash, so we can't recover a lost key — create a new one instead.

Access requires the **scraper** entitlement on your account (an admin enables it). A key inherits the permissions of the account it belongs to.

### Auth errors

| Status | Reason |
|---|---|
| `401 Unauthorized` | Header missing, key malformed, key revoked, key not found, or account inactive |
| `403 Forbidden` | Account does not have the scraper entitlement enabled |
| `429 Too Many Requests` | More than 60 requests in the last 60 seconds for this key |

```json
{ "error": "Unauthorized", "message": "API key has been revoked" }
```

---

## 2. Scrape endpoints

Both endpoints take a single `id` field — either a **bare handle/slug** or a **full LinkedIn URL** (passed through untouched). Every successful (and billable) call costs **1 request**.

### 2.1 POST `/scrape/person` — scrape a LinkedIn profile

`id` is the profile slug from `linkedin.com/in/<id>` (e.g. `rbranson`) or a full profile URL.

```bash
curl -X POST https://api.globedata.io/api/v1/scraper/scrape/person \
  -H "Authorization: Bearer $GD_KEY" \
  -H "Content-Type: application/json" \
  -d '{"id": "rbranson"}'
```

```json
{
  "success": true,
  "profile_type": "profile",
  "id": "rbranson",
  "requests_charged": 1,
  "requests_remaining": 4821,
  "credits_charged": 1,
  "credits_remaining": 4821,
  "data": { "...": "upstream profile record" }
}
```

### 2.2 POST `/scrape/company` — scrape a LinkedIn company

`id` is the company slug from `linkedin.com/company/<id>` (e.g. `amazon`) or a full company URL.

```bash
curl -X POST https://api.globedata.io/api/v1/scraper/scrape/company \
  -H "Authorization: Bearer $GD_KEY" \
  -H "Content-Type: application/json" \
  -d '{"id": "amazon"}'
```

```json
{
  "success": true,
  "profile_type": "company",
  "id": "amazon",
  "requests_charged": 1,
  "requests_remaining": 4820,
  "credits_charged": 1,
  "credits_remaining": 4820,
  "data": { "...": "upstream company record" }
}
```

> **`credits_*` vs `requests_*`:** the two are aliases (1 credit == 1 request). `requests_*` is the current, preferred naming; `credits_*` is kept for backward compatibility.

---

## 3. Account endpoints

### 3.1 GET `/credits` — your request balance

```bash
curl -H "Authorization: Bearer $GD_KEY" \
  https://api.globedata.io/api/v1/scraper/credits
```

```json
{
  "success": true,
  "requests_total": 5000,
  "requests_used": 180,
  "requests_remaining": 4820,
  "credits_total": 5000,
  "credits_used": 180,
  "credits_remaining": 4820,
  "cost_per_request": 1,
  "costs": { "company": 1, "profile": 1 }
}
```

### 3.2 GET `/history?days=N` — daily usage aggregate

Daily counts of your scrape requests, grouped by profile type. `days` defaults to `30`, max `365`.

```bash
curl -H "Authorization: Bearer $GD_KEY" \
  "https://api.globedata.io/api/v1/scraper/history?days=7"
```

```json
{
  "success": true,
  "days": 7,
  "usage": [
    {
      "date": "2026-07-22",
      "profile_type": "profile",
      "requests": 120,
      "successful": 118,
      "failed": 2,
      "credits": 120
    }
  ]
}
```

---

## 4. Reseller endpoints

These require the **reseller** role and let you distribute your request pool across your sub-users. (Non-resellers get `403 Forbidden`.)

### 4.1 GET `/subusers` — sub-user balances + free pool

```bash
curl -H "Authorization: Bearer $GD_KEY" \
  https://api.globedata.io/api/v1/scraper/subusers
```

```json
{
  "success": true,
  "credits_total": 10000,
  "credits_used": 180,
  "credits_allocated_to_subs": 3000,
  "credits_free": 6820,
  "subusers": [
    {
      "id": "3f2c…",
      "email": "team@example.com",
      "credits_total": 3000,
      "credits_used": 45,
      "credits_remaining": 2955
    }
  ]
}
```

### 4.2 POST `/subusers/:id/allocate-credits` — top up / reclaim

`credits_delta` is a signed integer. **Positive** tops the sub-user up (bounded by your free pool); **negative** reclaims (bounded by the sub-user's *unused* credits). `:id` is the sub-user's UUID.

```bash
curl -X POST https://api.globedata.io/api/v1/scraper/subusers/3f2c.../allocate-credits \
  -H "Authorization: Bearer $GD_KEY" \
  -H "Content-Type: application/json" \
  -d '{"credits_delta": 500}'
```

```json
{
  "success": true,
  "subuser_id": "3f2c…",
  "credits_total": 3500,
  "credits_used": 45,
  "credits_remaining": 3455
}
```

| Status | Reason |
|---|---|
| `404 Not Found` | Sub-user not found or not owned by you |
| `409 Conflict` | Not enough free credits to allocate, or trying to reclaim more than the sub-user's unused balance |

---

## 5. Billing

Billing is **per request**: every scrape costs **1 request**, whether it's a person or a company.

We only charge for outcomes our upstream provider bills us for:

| Upstream result | HTTP returned to you | Charged? |
|---|---|---|
| `200` profile found | `200` | ✅ Yes |
| `400` bad target / `404` not found | `400` / `404` | ✅ **Yes** — a valid-but-not-found lookup is still billable |
| `429` provider at capacity | `429` | ❌ No — retried internally; retry later |
| `5xx` / timeout / transport error | `503` | ❌ No |

So a `404` (the profile doesn't exist) **still costs 1 request**. A `429`/`503` costs nothing — the request is automatically refunded to your balance; just retry after a short wait.

### Billing-related errors

**`402 Payment Required`** — insufficient balance (nothing was scraped):

```json
{
  "error": "Payment Required",
  "message": "Insufficient requests: need 1, have 0",
  "required": 1,
  "available": 0
}
```

**`400` / `404` — charged upstream error.** The lookup failed but was billed; `charged: true` tells you the request was deducted:

```json
{
  "error": "Upstream Error",
  "message": "LinkedIn lookup returned 404; this request was charged (the provider bills 400/404).",
  "charged": true,
  "requests_charged": 1,
  "upstream_status": 404,
  "requests_remaining": 4819
}
```

**`429` — at capacity, not charged.** Retry shortly:

```json
{
  "error": "Too Many Requests",
  "message": "Scraper is at capacity; no request was charged. Please retry shortly.",
  "upstream_status": 429,
  "requests_remaining": 4820
}
```

**`503` — upstream unavailable, not charged.**

---

## 6. Errors at a glance

| Status | Meaning |
|---|---|
| `200` | Success — `data` holds the scraped record |
| `400` | Validation error (missing `id`), **or** a charged upstream bad-request |
| `401` | Auth failed (see [§1](#1-authentication)) |
| `402` | Insufficient request balance |
| `403` | Scraper entitlement missing, or reseller-only endpoint |
| `404` | Charged "not found" lookup, or (reseller) sub-user not found |
| `409` | Allocation conflict (reseller endpoints) |
| `429` | Rate-limited (your key) **or** upstream at capacity (not charged) |
| `503` | Scraper not configured, or upstream unavailable (not charged) |

---

## 7. Notes

- **Idempotency:** scrapes are not idempotent for billing — each call costs a request. Cache results on your side if you re-read the same profile often.
- **Retries:** we already retry non-billable outcomes (`429`/`5xx`/timeout) with backoff before returning. If you still get a `429`/`503`, wait a few seconds and retry — no request was charged.
- **Handles vs URLs:** both are accepted. `rbranson` and `https://www.linkedin.com/in/rbranson` are equivalent; `amazon` and `https://www.linkedin.com/company/amazon` are equivalent.
</content>
</invoke>
