Make your first API call
Check your key works, start an enrichment, read the result, and understand credits, limits and errors.
Written By Philip Poppe
Last updated About 3 hours ago
his article takes you from a fresh API key to your first enrichment result: check the key works, start an enrichment, and read the answer. You need a key first; see Create an API key.
The basics
Base URL:
https://api.surroundr.io. Every endpoint lives under/v1.Authentication: send your key as a bearer token on every request:
Authorization: Bearer srk_live_...Format: JSON in, JSON out. Send
Content-Type: application/jsonon requests with a body.Everything is live. There is no sandbox yet, so each enrichment you start spends real credits. The connection check below is free.
Step 1: check your key works
Before you start an enrichment, make one free call to confirm your key, your plan and your network path are all fine. Listing your webhook endpoints is a safe read: it changes nothing and spends no credits.
curl https://api.surroundr.io/v1/webhook_endpoints \ -H "Authorization: Bearer $SURROUNDR_API_KEY"What the answer tells you:
200with{"data": []}(or a list of your endpoints): the key works. You are ready.401 unauthorized: the key is missing, mistyped or revoked. Check you copied all of it and that the header readsBearer, a space, then the key.403 plan_denied: the key is valid but your plan no longer includes the API.404on every path: the API is not switched on for your account yet.
Step 2: start an enrichment
Tell us who the person is and what you want back:
curl https://api.surroundr.io/v1/enrichments \ -X POST \ -H "Authorization: Bearer $SURROUNDR_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8f14e45f-crm-4821" \ -d '{ "external_id": "crm-contact-4821", "person": { "first_name": "Jane", "last_name": "Doe", "linkedin_url": "https://www.linkedin.com/in/janedoe" }, "company": { "domain": "acme.com" }, "reveal": ["email", "phone"] }'What to send
reveal(required):["email"],["phone"]or both.personandcompany: enough to identify one person. That means at least one of these:the person's
linkedin_url,a known
emailfor the person,their
first_nameandlast_nametogether with the companydomainorname.
More is better. A LinkedIn URL gives the best match. Other optional fields are
full_name,job_titleandcountryon the person, andlinkedin_urlandcountryon the company. Countries are two-letter codes, likeBE.external_id(optional): your own reference, such as a CRM record id. We echo it back on the result and on webhooks so you can match them up. It does not need to be unique, and it does not stop duplicates.
Send an Idempotency-Key
If a request times out, you cannot tell whether we received it. Send a unique Idempotency-Key header per enrichment and retry with the same key: we return the original job and charge nothing extra. Keys are remembered for 24 hours. Reusing a key with a different body returns 409 idempotency_conflict. Without a key, a retried request starts, and charges for, a second job.
What comes back
202 Accepted with the job, straight away. Enrichment runs in the background, so the job usually starts as queued:
{
"id": "enr_3f2a9c0e7b1d4e5f8a6b2c1d0e9f8a7b",
"object": "enrichment",
"external_id": "crm-contact-4821",
"status": "queued",
"requested": ["email", "phone"],
"created_at": "2026-09-29T09:14:02.000Z",
"completed_at": null,
"credits_charged": 0,
"email": null,
"phone": null,
"failure": null
}Keep the id. If we already hold fresh data for this person, the 202 can already be the finished result.
Step 3: get the result
There are two ways to hear back.
Webhooks (recommended). We send the finished job to your server the moment it is ready. See Receive results with webhooks.
Polling. Ask for the job until it is done:
curl https://api.surroundr.io/v1/enrichments/enr_3f2a9c0e7b1d4e5f8a6b2c1d0e9f8a7b \ -H "Authorization: Bearer $SURROUNDR_API_KEY"While the job is still working, the response carries a Retry-After header in seconds. Wait that long before asking again. Once the status is final, stop: a finished job does not change.
A finished job looks like this:
{
"id": "enr_3f2a9c0e7b1d4e5f8a6b2c1d0e9f8a7b",
"object": "enrichment",
"external_id": "crm-contact-4821",
"status": "completed",
"requested": ["email", "phone"],
"created_at": "2026-09-29T09:14:02.000Z",
"completed_at": "2026-09-29T09:14:09.000Z",
"credits_charged": 11,
"email": {
"status": "found",
"value": "jane.doe@acme.com",
"type": "work",
"confidence": "verified",
"is_role": false,
"is_free_provider": false,
"verified_at": "2026-09-29T09:14:08.000Z"
},
"phone": {
"status": "found",
"value": "+32470123456",
"country": "BE",
"line_type": "mobile",
"confidence": "verified",
"do_not_call": false,
"verified_at": "2026-09-29T09:14:09.000Z"
},
"failure": null
}Job status
queued/running: still working. Result fields are empty.completed: everything you asked for was found.partial: some of it was found, for example an email but no phone.no_data: we searched and found nothing for this person.failed: the job could not finish. Thefailureobject says why and whether a retry makes sense (retryable).
Per channel
Each of email and phone has its own status: found, not_found, suppressed (found but withheld, for example a do-not-call number), blocked (not searched for compliance reasons in that region) or not_requested.
confidence tells you how sure we are: verified, probable, catch_all (the mail server accepts every address, so it cannot be fully confirmed) or unverified. Phone numbers are always in international format (E.164).
Credits
An email costs 1 credit, a mobile number 10 credits, the same as in the app.
You pay for what we find (and for a found value that is withheld as
suppressed). Nothing found, orblocked, costs nothing.credits_chargedon the job is what that job cost you.If the account does not have enough credits when you start a job, you get
402 insufficient_creditsand nothing is charged.
Limits
Request rate is counted per API key. Every response carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Resetso you can pace yourself.Jobs in progress are capped per account. Starting one more than the cap returns
429 concurrency_limited.Both
429responses carryRetry-After. Wait that many seconds, then retry.
The exact numbers depend on your plan.
Errors
Every error has the same body, so you can branch on code:
{ "code": "invalid_request", "message": "...", "field": "person" }400 malformed_json: the body is not valid JSON.401 unauthorized: missing, wrong or revoked key.402 insufficient_credits: top up and try again.403 plan_denied: your plan does not include the API.404 not_found: no job with that id on your account.409 idempotency_conflict: that Idempotency-Key was used with a different body.410 gone: the job is older than 30 days and has expired.422 invalid_request: the request breaks a rule.fieldpoints at the problem, for examplerevealorperson(not enough to identify someone).429 rate_limited/concurrency_limited: slow down, see Limits.500 internal_error/503 service_unavailable: our side. Retry later with the same Idempotency-Key.
A request that errors creates no job and costs nothing. Use the code, not the message, in your logic: codes never change, wording can.
How long results stay available
You can fetch a job for 30 days. After that it returns 410 gone, so store what you need on your side.