# Find Phone

Looks up a phone number for each LinkedIn profile you send, mobile first: up to 25 per call, each with its own entry in `results`, in the order you sent them.

You pay 10 credits for a person when a call returns a number for them, and only once: asking again for the same person returns the number for free. Nobody found, a lookup that is still running, or an error costs nothing. A person without a number is not looked up again for 30 days.

Most lookups answer within seconds; the call waits up to 30 seconds. A person still being looked up after that has the status `pending`: send the same URLs again after `retry_after_seconds`. That call charges only the numbers it returns.

The member-id URL that Search People returns works too. Set your HTTP timeout to at least 60 seconds.

## Authentication

Send your API key as a Bearer token: `Authorization: Bearer gtk_…`. Create the key under **Connect assistant → API keys**. A missing, invalid or revoked key is **401**, including while an API is closed. On an endpoint of the Signals API or the Company API, a valid key while that API is closed for your organization answers **403** `api_not_enabled`, with what the key can do instead; the other endpoints work with every key. Every response includes `mySDR-Api-Version`, `mySDR-Request-Id` and `Cache-Control: no-store`, and a response to an accepted key also `X-RateLimit-*` (so not a 401, a 405, an OPTIONS or a 404 for an unknown path); every error has `type`, `message`, `param` and `request_id`.

### Body parameters

- **`profile_urls`** `string[]` · **required** — 1 to 25 LinkedIn profile URLs, e.g. `https://www.linkedin.com/in/jane-example`, or the member-id URL from Search People.
- **`max_credits`** `integer` — Stop before the call costs more than this. A number you already paid for, and a person nobody found a number for in the last 30 days, are free and take no room. Everyone else counts at 10 credits, in the order you sent them, and a person for whom nothing was found leaves the room to the next; who still does not fit is not looked up and gets `skipped`. Below 10, with nothing free to return, the call is `402` `max_credits_exceeded` and nothing is looked up.

## Response

- **`results`** `object[]` — One entry per URL you sent, in the same order.
- **`results[].input`** `string` — The URL as you sent it.
- **`results[].status`** `string` — `found` (see `phones`), `not_found` (no number known for this person; not looked up again for 30 days), `pending` (still being looked up: send the URL again after `retry_after_seconds`), `invalid_url` (not a LinkedIn profile URL), `error` (the lookup failed; nothing was charged, retry later) or `skipped` (`max_credits` left no room; not looked up). One of: `found`, `not_found`, `pending`, `invalid_url`, `error`, `skipped`.
- **`results[].linkedin_url`** `string | null` — The public profile URL that was looked up. Null for an invalid URL, or a member-id URL whose profile does not exist.
- **`results[].phone`** `string | null` — The best number: a mobile number first, then the one with the highest confidence. Null unless the status is `found`.
- **`results[].phones`** `object[]` — Every number found, best first: `number` as the source writes it, `type` (`mobile`, `work` or null when unknown) and `confidence` (`high`, `likely` or null). Empty unless the status is `found`.
- **`results[].credits_charged`** `integer` — What this entry cost: 10 when this call returned a number for the first time, otherwise 0. A URL you sent twice is charged on its first entry.
- **`results[].error`** `object` — With `error` and `skipped`: `type` (`upstream_error` or `max_credits_exceeded`) and `message`.
- **`credits_used`** `integer` — Credits this call used.
- **`retry_after_seconds`** `integer` — Only when an entry is `pending`: send those URLs again after this many seconds (60).

## Credits

10 credits per person with a phone number found, once per person; nothing found costs nothing. Errors and empty lookups are free. A call that spent credits puts that number in `credits_used`, in `X-Credits-Used` and in `X-Credits-Charged`. When the wallet can be read, the response also sends `X-Credits-Remaining`.

## Errors

- **400** `invalid_request` — The body is not valid JSON, a field is unknown or has the wrong type, or a value is not allowed.
- **401** `authentication_error` — The Authorization header is missing, or the key is unknown or revoked.
- **403** `permission_error` — API access is not part of your plan, or your account is suspended.
- **429** `rate_limit_error` — More than 60 requests per minute, or more than 3 at the same time, on one key. Wait for the Retry-After header.
- **429** `daily_limit_reached` — The request could take your organization over its daily credit limit. The limit resets at midnight (Europe/Brussels).
- **502** `upstream_error` — The data source did not answer. Retry later.
- **503** `service_unavailable` — The API is temporarily unavailable. Retry later.
- **500** `internal_error` — Something went wrong on our side.
- **402** `max_credits_exceeded` — max_credits is below the price of one phone number (10 credits). Nothing was looked up.

## Example

### cURL

```bash
curl -X POST "https://mysdr.app/api/v1/person/phone" \
  -H "Authorization: Bearer $MYSDR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profile_urls": [
      "https://www.linkedin.com/in/jane-example",
      "https://www.linkedin.com/in/john-example"
    ]
  }'
```

### JavaScript

```javascript
const response = await fetch('https://mysdr.app/api/v1/person/phone', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.MYSDR_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "profile_urls": [
      "https://www.linkedin.com/in/jane-example",
      "https://www.linkedin.com/in/john-example"
    ]
  })
});
const body = await response.json();
```

### Python

```python
import json
import os
import urllib.request

payload = {
  "profile_urls": [
    "https://www.linkedin.com/in/jane-example",
    "https://www.linkedin.com/in/john-example"
  ]
}

request = urllib.request.Request(
    "https://mysdr.app/api/v1/person/phone",
    headers={
    "Authorization": f"Bearer {os.environ['MYSDR_API_KEY']}",
    "Content-Type": "application/json"
    },
    data=json.dumps(payload).encode(),
    method="POST",
)
with urllib.request.urlopen(request) as response:
    body = json.load(response)
```

### Example response

```json
{
  "results": [
    {
      "input": "https://www.linkedin.com/in/jane-example",
      "status": "found",
      "linkedin_url": "https://www.linkedin.com/in/jane-example",
      "phone": "+32 470 12 34 56",
      "phones": [
        {
          "number": "+32 470 12 34 56",
          "type": "mobile",
          "confidence": "high"
        },
        {
          "number": "+32 9 123 45 67",
          "type": "work",
          "confidence": "likely"
        }
      ],
      "credits_charged": 10
    },
    {
      "input": "https://www.linkedin.com/in/john-example",
      "status": "pending",
      "linkedin_url": "https://www.linkedin.com/in/john-example",
      "phone": null,
      "phones": [],
      "credits_charged": 0
    }
  ],
  "credits_used": 10,
  "retry_after_seconds": 60
}
```
