Developers
Prospexly API
A REST API over the same records the app uses: contacts, companies, opportunities, tasks and AI lead search. JSON in, JSON out, and field names that match the CRM’s own columns rather than a translation layer you have to learn.
Machine-readable description: openapi.json (OpenAPI 3.1). Point a client generator at it rather than writing request types by hand.
Authentication
Every request carries a key created in Settings. Keys are stored only as an Argon2id hash, so the value is shown once and cannot be recovered afterwards.
curl https://prospexly.com/api/v1/contacts?page_size=10 \
-H "Authorization: Bearer pxly_YOUR_KEY"Base URL https://prospexly.com/api/v1. An absent, malformed, revoked or expired key is a 401. A key that is valid but lacks the scope an endpoint needs is a 403, whatever your plan.
Scopes
Each key carries an explicit set of scopes chosen at creation. Grant the fewest that do the job — a key that only reads cannot be used to delete your pipeline.
Contacts
contacts.readcontacts.write
Companies
companies.readcompanies.write
Opportunities
opportunities.readopportunities.write
Tasks
tasks.readtasks.write
Lead search
leads.search
Rate limits
Two limits apply. A burst limit of 500 requests a minute per key, and a daily limit set by your plan.
| Plan | Requests per day |
|---|---|
| Free | 100 |
| Starter | 1,000 |
| Professional | 10,000 |
| Enterprise | Unlimited |
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, the last as a Unix timestamp in seconds. The daily window resets at midnight UTC. A refused request is not counted against you.
Paging and filtering
List endpoints return 25 rows by default and never more than 100. Asking for more is clamped rather than rejected, so a client that requests 5000 gets 100 and a truthful pagination block rather than an error it has to handle.
{
"data": [ { "id": "...", "first_name": "Ada", "last_name": "Lovelace" } ],
"pagination": {
"page": 1,
"page_size": 25,
"total": 118,
"page_count": 5,
"has_next": true,
"has_previous": false
}
}Unrecognised sort, direction and filter values fall back to the default instead of erroring — a saved query should keep working after a column is renamed. Unknown fields in a request body are the opposite: they are rejected with a 400, because a typo there means you believe you set something you did not.
Endpoints
Contacts
| Method | Path | Scope |
|---|---|---|
| GET | /contactsList contacts | contacts.read |
| POST | /contactsCreate a contact | contacts.write |
| GET | /contacts/{id}Retrieve a contact | contacts.read |
| PATCH | /contacts/{id}Update a contact | contacts.write |
| DELETE | /contacts/{id}Delete a contact | contacts.write |
Companies
| Method | Path | Scope |
|---|---|---|
| GET | /companiesList companies | companies.read |
| POST | /companiesCreate a company | companies.write |
| GET | /companies/{id}Retrieve a company | companies.read |
| PATCH | /companies/{id}Update a company | companies.write |
| DELETE | /companies/{id}Delete a company | companies.write |
Opportunities
| Method | Path | Scope |
|---|---|---|
| GET | /opportunitiesList opportunities | opportunities.read |
| POST | /opportunitiesCreate a opportunity | opportunities.write |
| GET | /opportunities/{id}Retrieve a opportunity | opportunities.read |
| PATCH | /opportunities/{id}Update a opportunity | opportunities.write |
| DELETE | /opportunities/{id}Delete a opportunity | opportunities.write |
Tasks
| Method | Path | Scope |
|---|---|---|
| GET | /tasksList tasks | tasks.read |
| POST | /tasksCreate a task | tasks.write |
| GET | /tasks/{id}Retrieve a task | tasks.read |
| PATCH | /tasks/{id}Update a task | tasks.write |
| DELETE | /tasks/{id}Delete a task | tasks.write |
Lead search
| Method | Path | Scope |
|---|---|---|
| GET | /lead-searchesList lead searches | leads.search |
| POST | /lead-searchesStart a lead search | leads.search |
| GET | /lead-searches/{id}Retrieve a lead search | leads.search |
Creating and updating
A create returns 201 with the new record and a Location header. Records made through the API are marked with source api, so you can tell an integration’s rows from a manual entry inside the CRM.
curl -X POST https://prospexly.com/api/v1/contacts \
-H "Authorization: Bearer pxly_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@example.com",
"role": "Head of Engineering"
}'A PATCH changes only the fields you send. There is no PUT: a full replace makes it far too easy to blank a field by omitting it.
curl -X PATCH https://prospexly.com/api/v1/contacts/CONTACT_ID \
-H "Authorization: Bearer pxly_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "engaged"}'Contacts carry first_name and last_name, never a combined name. There is no full-name field to write to, deliberately, and there will not be one.
AI lead search
The one asynchronous endpoint. A grounded search takes 10–60 seconds and longer when it retries, so it returns 202 with a status_url to poll rather than holding the connection open.
# Start the search. Returns 202 immediately.
curl -X POST https://prospexly.com/api/v1/lead-searches \
-H "Authorization: Bearer pxly_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"industry": "Dental practices",
"country": "United Kingdom",
"location": "Manchester",
"max_results": 10
}'
# Then poll until status is "completed" or "failed". A clean run takes about 40
# seconds; one that retries can take twice that.
curl https://prospexly.com/api/v1/lead-searches/SEARCH_ID \
-H "Authorization: Bearer pxly_YOUR_KEY"One company or one contact counts as one AI lead, so a search for ten companies can cost up to twenty. If your remaining allowance cannot cover the request it is trimmed to what it can, and clamped_from in the response reports what you originally asked for. An exhausted allowance is a 402, not a 429 — it will not clear on its own before the billing period does.
Errors
Every failure has the same shape, with a stable code to branch on and a message written to be read by a person.
{
"error": {
"code": "forbidden",
"message": "This key does not have the `contacts.write` scope."
}
}A record belonging to another account is a 404 rather than a 403. That is not evasion: 403 would confirm the id exists, which is enough to enumerate other customers’ record ids one request at a time.
Questions
- Where do I get a key?
- Settings, in the app. The key is shown once at creation and stored only as an Argon2id hash, so we cannot show it to you again or recover it — if you lose it, revoke it and create another.
- What happens when I hit the daily limit?
- A 429 with `X-RateLimit-Reset` carrying the Unix timestamp at which the window reopens, and a `Retry-After` in seconds. Refused calls are not counted, so being over the limit does not push the reset further away.
- Do API calls use my AI lead allowance?
- Only lead search does, and it costs exactly what the same search costs in the app: one company or one contact is one lead. Everything else — contacts, companies, opportunities, tasks — is ordinary CRM data and costs nothing beyond the daily call limit.
- What happens if my lead allowance runs out mid-month?
- `POST /lead-searches` returns 402 rather than 429, because it will not clear on its own in a minute. Buy a credit pack on the subscription page and it applies immediately — credits are spent after the monthly allowance and never expire, so there is no penalty for topping up early. Retrying a 402 without buying credits will keep returning 402.
- Is there a sandbox?
- No. There is one environment, and a key scoped to read-only permissions is the safe way to explore it. A sandbox that does not share the production data model teaches you the sandbox, and creating a second environment for you to get wrong is not a favour.
- Are webhooks available?
- Not yet. Lead search is the only long-running operation and it is polled from `status_url`. Nothing else in the API takes long enough for a callback to be worth the delivery guarantees it would need.
Build against it
Create a key in Settings, give it the narrowest scopes that do the job, and point your client generator at the OpenAPI document.
