In this article:
⚠️ IMPORTANT: The SingleOps Support team will provide one API token per account/request, but it is up to the account to create and maintain the connection. We offer the following documentation, but cannot offer support related to creating, testing, or resolving issues with the use of the API token.
What is the Lead Entry API?
The Lead Entry API lets you create new leads in SingleOps directly from an outside system — for example, your website, a marketing form, or another CRM. Instead of re-keying the same information in two places, leads flow straight into SingleOps, which cuts down on duplicate data entry and keeps your team moving faster.
This is a Lead Entry API only. It's scoped to creating portal leads (and, optionally, a task alongside them). It is not a general-purpose job-creation or read/write API, and there is no native Zapier integration — the connection is a developer integration you build and maintain.
Because it's a developer integration, you'll need someone technical on your team to set it up and maintain it. If that's not something you have in-house, our Client Portal for Lead Entry does the same job directly from your website with no code required.
⚠️ Leads created through this API follow the Customer Portal workflow, which means a client or location must be manually confirmed in SingleOps before it's considered valid.
Before you start
You'll need two things in place:
A technical resource on your team. SingleOps provides the steps to request an API token, but connecting the integration, troubleshooting it, and maintaining it going forward all happen on your end.
An API access token. Email support@singleops.com to have a new API User created on the account where you want leads to land. You'll receive an email address and an API token — keep both handy, since every request uses them. This API user does not incur any additional cost.
How it works
Creating a lead is a short sequence of requests. At a high level:
Look up reference IDs — grab the operation and custom input IDs your account uses. These identify which division a lead belongs to and any extra fields you track. You only need to do this occasionally (and can cache the results).
Search for the client — check whether the lead matches an existing client by email or phone.
Create the lead — send the lead itself, either attached to the existing client you found or as a brand-new client. Optionally, you can create a task at the same time.
The rest of this article walks through each step with the exact endpoints and examples.
Authentication
Credentials
Every request is authenticated with the two values from your API user:
user_email:
user_token:
For most requests these go in the message body. GET requests can also accept them as URL parameters.
Account groups
Each account has its own unique API user. An API user can only create leads on the account it belongs to — even if the underlying person has access to other accounts in the same account group, the API user cannot make calls to those other accounts.
Example: GET request with URL parameters
curl -X 'GET' \
'https://app.singleops.com/api/v1/custom_inputs?user_email=&user_token=' \
-H 'accept: application/json'
Fill in your user_email and user_token, and your request will look like this:
curl -X GET \
"https://app.singleops.com/api/v1/custom_inputs?user_email=accountname%2Bapiuser@singleops.com&user_token=YOUR_TOKEN_HERE" \
-H "accept: application/json"
Important: encode the + in your email
API user emails are usually set up with plus-addressing, like accountname+apiuser@singleops.com. When you put that email in a URL, the + must be URL-encoded as %2B.
This matters because a raw + in a URL is read as a space. If you paste your email in unencoded:
?user_email=accountname+apiuser@singleops.com
the server actually receives accountname apiuser@singleops.com (with a space), authentication fails, and you'll get a confusing error. Encoding the + as %2B fixes it:
?user_email=accountname%2Bapiuser@singleops.com
The @ does not need to be encoded — a literal @ is accepted in the query string. The + is the only character here that has to be changed.
Testing in your browser
For quick testing of GET requests, you can use your normal logged-in session (your user cookie) to hit the API endpoints, with two conditions:
Requests must be made over HTTPS.
You must be viewing the correct account. If you belong to an account group and are currently looking at a different account in-app, switch back to your "home" account before you can access its data.
Step 1: Look up reference IDs
Two IDs are required on every lead: an operation ID and, if you use them, custom input IDs. We recommend caching these, but you can also fetch them fresh for each lead.
Operations
An operation is the division a lead belongs to. It's required on every lead, though it can be reassigned later if it's mis-classified.
Endpoint: GET https://app.singleops.com/api/v1/operations No additional parameters needed.
Response:
[
{
"id": 1234,
"name": "Main",
"active": true,
"tax_location_id": null,
"display_name": null,
"location_id": null,
"location": {}
},
{
"id": 1235,
"name": "Other",
"active": true,
"tax_location_id": null,
"display_name": null,
"location_id": null,
"location": {}
}
]
Custom inputs
Custom inputs are extra fields your account defines to capture additional information on leads, jobs, clients, items, and more — for example, a "Lead Source" field.
Endpoint: GET https://app.singleops.com/api/v1/custom_inputs
Response:
[
{
"id": 1234,
"name": "Main",
"active": true,
"tax_location_id": null,
"display_name": null,
"location_id": null,
"location": {}
},
{
"id": 1235,
"name": "Other",
"active": true,
"tax_location_id": null,
"display_name": null,
"location_id": null,
"location": {}
}
]
You can ignore dropdown_options. It lists the choices a custom input supports, but since your lead source will typically be the same value every time, you won't need it.
Step 2: Search for an existing client
Before creating a lead, search for a matching client by email or phone so you don't create a duplicate.
Endpoint: GET https://app.singleops.com/api/v1/clients/search_by_field
Parameters:
search_field: "email" or "phone"
search_term: [the email address or phone number]
A few things to know:
The search is a prefix search. Clients sometimes store multiple phone numbers or extensions in one field, so a prefix match helps catch those.
A search can return anywhere from 0 to 50 results. If there are more, use the
pageparameter to page through them.
Why this step matters: the API does not merge records.
• If you pass client_id, the lead attaches directly to that existing client.
• If you leave client_id out, the lead is held against a shared placeholder client ("Anonymous Portal Client"), and the details you sent under portal_lead stay on the lead itself. A real client is created only when someone confirms the lead in SingleOps, and it's created fresh, with no matching against your existing clients.
Skipping this search doesn't fail immediately, it defers the problem: every unconfirmed lead looks unattached in the app, and confirming them creates duplicates for customers you already have.
Response (one match shown):
[
{
"id": 12345,
"account_id": 123,
"name": "Example Client",
"active": true,
"email": "support@singleops.com",
"phone": "555-555-5555",
"first_name": "Test",
"last_name": "Search",
"company_name": "",
"portal_active": true,
"on_hold": false
}
]
(The full response includes many more fields; the ones above are the most relevant for identifying a match.)
Once you've searched, you'll know which path to take in the next step: attach the lead to the existing client you found, or create it with a new client.
Step 3: Create the lead
All leads are created by sending a POST request with a JSON body to:
https://app.singleops.com/api/v1/jobs
Choose the version below that matches your situation.
Required fields: every lead needs operation_id, plus a way to identify the client — either a client_id (existing client) or client details under portal_lead (new client). Everything else is optional.
About visit_stage_id: this sets the stage the lead lands in. Use 8 — the Lead stage. Stage IDs are the same across all SingleOps accounts. If you leave visit_stage_id out, the request won't create a lead, so include it.
For an existing client
Include the client_id you found in Step 2.
{
"user_email": "",
"user_token": "",
"job": {
"client_id": 12345,
"operation_id": 123,
"name": "Example Lead",
"job_notes": "Here are notes that are internal.",
"client_notes": "Here are notes that the client can see.",
"crew_notes": "Here are notes that are on the work order.",
"custom_input_values": [
{ "custom_input_id": 123, "value": "Email" }
]
},
"portal_lead": {
"address": "101 Test Ave",
"city": "Atlanta",
"us_state": "GA",
"zip": "30339"
},
"visit": {
"visit_stage_id": 8
}
}
Response — confirms whether the lead was created and returns its ID:
{
"success": true,
"error": null,
"id": 123456
}
For a new client
Leave client_id out of the job object. Instead, put the new client's details — including phone, email, and mobile — under portal_lead.
{
"user_email": "",
"user_token": "",
"job": {
"operation_id": 916,
"name": "Example Lead",
"job_notes": "Here are notes that are internal.",
"client_notes": "Here are notes that the client can see.",
"crew_notes": "Here are notes that are on the work order.",
"custom_input_values": [
{ "custom_input_id": 463, "value": "Email" }
]
},
"portal_lead": {
"first_name": "Example",
"last_name": "Lead",
"company_name": "LeadCo LLC",
"email": "new-lead@example.com",
"phone": "555-555-5555",
"address": "101 Test Ave",
"city": "Atlanta",
"us_state": "GA",
"zip": "30305",
"bill_addr_1": "200 Main St",
"bill_addr_2": "Apt 2B",
"bill_addr_state": "GA",
"bill_addr_postal_code": "30367",
"bill_addr_city": "Marietta"
},
"visit": {
"visit_stage_id": 8
}
}
Creating a lead with a task
To create a task at the same time as the lead, add two things to the body above:
Set
"job_task_create": trueInclude a
taskobject
{
"user_email": "",
"user_token": "",
"job": {
"operation_id": 916,
"name": "Example Lead",
"job_notes": "Here are notes that are internal.",
"client_notes": "Here are notes that the client can see.",
"crew_notes": "Here are notes that are on the work order.",
"custom_input_values": [
{ "custom_input_id": 463, "value": "Email" }
]
},
"portal_lead": {
"first_name": "Example",
"last_name": "Lead",
"company_name": "LeadCo LLC",
"email": "new-lead@example.com",
"phone": "555-555-5555",
"address": "101 Test Ave",
"city": "Atlanta",
"us_state": "GA",
"zip": "30305",
"bill_addr_1": "200 Main St",
"bill_addr_2": "Apt 2B",
"bill_addr_state": "GA",
"bill_addr_postal_code": "30367",
"bill_addr_city": "Marietta"
},
"visit": {
"visit_stage_id": 8
},
"job_task_create": true,
"task": {
"name": "Task name",
"notes": "Internal Notes from Lead",
"on_site": true,
"start": "2021-10-10 07:00:00",
"stop": "2021-10-10 07:30:00",
"all_day": false
}
}Response:
{
"success": true,
"error": null,
"id": 148830
}
Appendix: Field reference
Character limits and accepted values for each field.
Job
Field | Limit |
name | value: text limits: 41 characters |
custom_input_values.value | value: text limits: 65,535 characters (currently listed as 4,000) |
operation_id |
|
client_id | value: integer — the client ID from the Step 2 search |
sales_rep_ids |
|
custom_input_values.custom_input_id |
|
job_notes |
|
client_notes |
|
crew_notes |
|
Visit
Value | Limit |
visit_stage_id |
|
tag_ids |
|
Portal lead
Field | Limit |
first_name | 255 characters |
last_name | 255 characters |
company_name | 255 characters |
255 characters | |
phone | 255 characters |
mobile | 255 characters |
address | 255 characters |
city | 255 characters |
us_state | value: text 5 characters (state / province codes) |
zip | 255 characters |
middle_name | value: text limits: 255 characters |
bill_addr_1, bill_addr_2, bill_addr_3, bill_addr_city, bill_addr_state, bill_addr_postal_code | value: text limits: 255 characters each |
Task
Field | Value |
job_task_create | true or false |
task.name | value: text limits: 255 characters |
task.notes | value: text limits: 65,535 characters |
task.on_site | true or false |
task.start | DateTime |
task.stop | DateTime |
task.all_day | true or false |
