What it does
Four objects go in — Accounts, Contacts, Deals and Activities. They land in the same records Salespuzzle’s own CRM, Sales Pulse, Deal Compass and Account Compass read, so anything you send is immediately part of the analysis.
Your CRM stays the system of record. This API consumes and analyses. It does not write back into Pipedrive, HubSpot or SugarCRM, and it has no delete verb.
| Salespuzzle | Pipedrive | HubSpot | SugarCRM |
|---|---|---|---|
| Account | Organization | Company | Account |
| Contact | Person | Contact | Contact |
| Deal | Deal | Deal | Opportunity |
| Activity | Activity | Call / Email / Meeting / Note / Task | Call / Meeting / Task / Note |
An Opportunity and a Deal are one record in Salespuzzle at different stages, not two objects. A task is an Activity whose type is task — there is no separate task object.
Before you start: you need a relay
None of Pipedrive, HubSpot or SugarCRM can call this API directly from its own built-in webhook feature. Each fails for a different reason. Use Make or Zapier between your CRM and Salespuzzle. This is not something you can configure around, so plan for it rather than discovering it halfway through.
| CRM | Why a relay is needed | Plan required |
|---|---|---|
| Pipedrive | The Automations webhook action has four fields: Webhook, Method, Body and Path. There is no Headers field, so it cannot send your token. The platform Webhooks API offers only HTTP Basic. | Automations need Growth or above. All four plans were renamed in November 2025, so “Advanced” no longer exists. |
| HubSpot | It can send an Authorization header. What it cannot send is nested JSON — the body builder is flat pairs only, and this API needs a records array. A Custom Code action can build it and call us directly. |
The webhook action needs Data Hub Professional or Enterprise. Operations Hub was renamed Data Hub in September 2025. |
| SugarCRM | SugarBPM has no web-service action and never has. Web Logic Hooks do exist, with five fields, no authentication option, and a fixed Sugar envelope. A scheduled pull against the REST API is the most reliable route. | Target Sugar 25.1 or 26.1. Sugar 14.0 left support in April 2026. |
Make or Zapier? Make’s HTTP Make a request module allows custom headers with no documented plan gate, so it is usually the cheaper choice for a small business. Zapier’s Webhooks by Zapier also allows custom headers but is not available on the Free plan.
Get a key
In Salespuzzle: Company Admin → Integrations → New key. Only the Company Owner and Company Admins can issue one. The key is shown once and is not recoverable. If it is lost, revoke it and issue another.
Keys are scoped per object and action, so each integration can be given the least it needs.
| Scope | What it allows |
|---|---|
| accounts:write | Create and update Accounts |
| contacts:write | Create and update Contacts |
| deals:write | Create and update Deals |
| activities:write | Create and update Activities |
| accounts:read | Read Accounts back — and the same for the other three |
| *:read / *:write | Every object, including any added later |
Writing does not allow reading. A key that only pushes records into Salespuzzle cannot pull your database back out through the same credential.
The five rules
1. Every record needs an external_id
Your own system’s id for that record. Sending the same id again updates that record; it never creates a second one. This is the entire basis of the API.
2. Every request needs a source
A short slug naming the system the records came from — pipedrive, hubspot, sugarcrm. It forms part of the upsert key, so two systems can both use the id “42” without colliding. Keep it identical on every sync or you will create duplicates.
3. Link by the other record’s external_id
A contact says "account_external_id": "org-9", never a Salespuzzle id. Send Accounts before Contacts and Deals. A link that cannot be resolved yet is reported back and the record is still stored, so the next sync connects it.
4. There is no delete
Send "archived": true for a record removed in your CRM, and false to restore it. A record removed upstream must not take the Salespuzzle history that referenced it with it.
5. Unrecognised fields are kept, not dropped
They are stored against the record as additional fields and registered on your company’s field list, so the same column arrives in the same place every time. Activities are the exception — they have nowhere to keep extras and will say so in the response.
Authentication
One header on every request. Both Bearer <key> and a bare key are accepted, because integration tools differ.
# Prove the key and see which company it belongs to
curl https://mysalespuzzle.app/api/v1/ping \
-H "Authorization: Bearer sp_live_..."
The response names the company. Check it is the one you expect before sending 400 records into it.
{
"ok": true,
"company": "Apex Retail Group",
"key": { "name": "Pipedrive sync", "prefix": "K7mPq2Rd" },
"scopes": ["accounts:write", "contacts:write", "deals:write"],
"server_time": "2026-09-16T09:14:02.118Z"
}
Endpoints
The schema endpoint is the authority, not this page. It is generated from the same code that stores your data, and returns the complete alias list for every field. Where this page and that endpoint disagree, the endpoint is right. Add ?object=deals to narrow it.
{object} is accounts, contacts, deals or activities. Create and update are the same call, because an integration cannot know which one it needs and should not have to.
curl -X POST https://mysalespuzzle.app/api/v1/deals/upsert \
-H "Authorization: Bearer sp_live_..." \
-H "Content-Type: application/json" \
-d '{
"source": "pipedrive",
"records": [
{
"external_id": "1042",
"name": "Q4 renewal - Apex Retail",
"account_external_id": "org-9",
"amount": 24000,
"currency": "GBP",
"stage": "negotiation",
"expected_close_date": "2026-11-30"
}
]
}'
Every record is reported back by its own id, so you can reconcile without guessing which of a hundred failed.
{
"ok": true,
"object": "deals",
"source": "pipedrive",
"received": 1,
"created": 1,
"updated": 0,
"failed": 0,
"results": [
{ "external_id": "1042", "action": "created", "id": "..." }
]
}
A failed record does not sink the batch. You get a 200 with that record listed under failed and an errors array naming it. Losing 99 good records because the hundredth had an unmappable stage would be the wrong outcome.
| Parameter | Meaning |
|---|---|
| updated_since | ISO 8601. Everything changed since then. |
| cursor | From the previous page’s next_cursor. |
| limit | Default 100, maximum 500. |
| source | Only records from one source system. |
| archived | true includes archived records; only returns just those. |
Paging is by cursor, not offset, so a record cannot be skipped while the list changes underneath you. Archived records are excluded unless you ask for them.
Errors and limits
Every failure returns the same shape, so one error handler covers all of them: { "ok": false, "error": "..." }.
| Status | Meaning |
|---|---|
| 400 | Something in the request is wrong. The message says what. |
| 401 | No key, or not a valid one. |
| 403 | The key lacks the scope. The message lists what it does hold. |
| 404 | No such object. |
| 405 | Wrong verb. The body shows the right one. |
| 413 | More than 100 records in one request. |
| 429 | More than 60 requests a minute on this key. Batch, do not loop. |
| 5xx | Our fault. Nothing was stored, and the call is safe to repeat. |
Every request appears in Company Admin → Integrations with what it created, updated or could not store, so you can diagnose an integration without contacting us.
Accounts
The companies you sell to. A Pipedrive Organization, a HubSpot Company or a SugarCRM Account all map here.
Send website wherever you have it. It is the only thing that puts a company’s logo on its record. Salespuzzle never guesses a domain from a company name.
| Field | Type | Notes and common aliases |
|---|---|---|
| external_id required | string | Your system’s id. Also id, account_id. |
| name required | string | account_name, company_name, organisation |
| website | string | domain, url, website_url |
| industry | string | sector |
| sub_sector | string | |
| company_size | string | employees, numberofemployees |
| annual_revenue | number | revenue, turnover, annualrevenue |
| phone | string | telephone, phone_office |
| string | ||
| address | string | street, billing_address_street |
| city | string | billing_city |
| state | string | county, billing_state |
| postal_code | string | postcode, zip |
| country | string | billing_country |
| account_owner | string | The owner’s name, free text. The API never creates users. |
| account_tier | string | tier, account_type |
| account_region | string | region |
| status | enum | active · inactive · archived |
| currency | string | ISO 4217, e.g. GBP |
| current_arr | number | arr |
| customer_since | date | |
| contract_start_date | date | |
| contract_end_date | date | |
| renewal_date | date | |
| licenses_purchased | integer | licences_purchased |
| primary_contact_name | string | |
| primary_contact_email | string | |
| primary_contact_role | string | |
| last_activity_date | date | |
| notes | text | description, comments |
| tags | string[] | An array, or a comma-separated string |
| archived | boolean | Archive or restore the record |
Contacts
The people at those companies. A Pipedrive Person, a HubSpot Contact, a SugarCRM Contact.
| Field | Type | Notes and common aliases |
|---|---|---|
| external_id required | string | id, contact_id |
| account_external_id link | string | The Account’s external_id, same source. org_id |
| first_name required | string | firstname, forename |
| last_name | string | lastname, surname |
| string | email1, work_email | |
| phone | string | phone_work, telephone |
| mobile | string | mobilephone, phone_mobile |
| title | string | jobtitle, position |
| department | string | team |
| seniority | enum | c_level · vp · director · manager · ic · unknown |
| location | string | office |
| linkedin_url | string | linkedin |
| twitter_url | string | twitter |
| contact_status | enum | active · inactive · bounced · do_not_contact · unknown |
| lead_status | enum | new · contacted · qualified · proposal · negotiation · closed_won · closed_lost |
| lead_score | integer | hubspotscore |
| notes | text | description |
| tags | string[] | labels |
| archived | boolean |
Deals
Open and closed revenue. A Pipedrive Deal, a HubSpot Deal, a SugarCRM Opportunity.
| Field | Type | Notes and common aliases |
|---|---|---|
| external_id required | string | id, deal_id |
| name required | string | dealname, title, opportunity_name |
| account_external_id link | string | The Account’s id. org_id |
| contact_external_id link | string | The Contact’s id. person_id |
| amount | number | value, deal_value |
| currency | string | deal_currency_code |
| stage | enum | See the mapping table below |
| probability | integer | win_probability |
| expected_close_date | date | closedate, date_closed |
| actual_close_date | date | closed_date |
| assigned_to | string | The owner’s name. assigned_user_name |
| source | string | lead_source, hs_analytics_source |
| next_step | string | hs_next_step |
| forecast_category | string | |
| product_name | string | Free text. Line items are not imported here. |
| quantity | number | qty |
| unit_price | number | price |
| description | text | |
| notes | text | comments |
| tags | string[] | labels |
| archived | boolean |
Mapping your pipeline
Your stage names will not match these. Map them at your end. A stage Salespuzzle does not recognise is rejected with the accepted list, rather than quietly stored as something the pipeline cannot read.
| Salespuzzle | HubSpot default pipeline | SugarCRM sales_stage | Pipedrive |
|---|---|---|---|
| prospecting | appointmentscheduled | Prospecting | your stage 1 |
| qualification | qualifiedtobuy | Qualification, Needs Analysis | your stage 2 |
| discovery | presentationscheduled | Value Proposition | your stage 3 |
| proposal | decisionmakerboughtin | Proposal/Price Quote | your stage 4 |
| negotiation | contractsent | Negotiation/Review | your stage 5 |
| closed_won | closedwon | Closed Won | status = won |
| closed_lost | closedlost | Closed Lost | status = lost |
Pipedrive stages are numeric ids per pipeline, and HubSpot custom pipelines work the same way, so fill in the right-hand column for the account you are connecting.
Activities
Calls, meetings, emails, notes and tasks. Activities are the one object with nowhere to keep extra fields — anything unrecognised is reported in the response under warnings rather than stored.
| Field | Type | Notes and common aliases |
|---|---|---|
| external_id required | string | id |
| activity_type required | enum | call · meeting · online_meeting · email · text · linkedin · task · note · other |
| subject | string | title, name |
| description | text | note, body |
| activity_date | datetime | due_date, date_start, hs_timestamp |
| duration_minutes | integer | Minutes. Convert from HH:MM or milliseconds at your end. |
| status | enum | open · completed |
| direction | enum | inbound · outbound |
| outcome | string | hs_call_disposition |
| account_external_id link | string | org_id |
| contact_external_id link | string | person_id |
| deal_external_id link | string | |
| archived | boolean |
open is work still to do; completed is logged history. An open activity appears in the rep’s day in Salespuzzle. A Pipedrive activity with done: true is completed; a SugarCRM call marked Planned is open.
Pipedrive lunch maps to meeting, deadline to task. A HubSpot note is note. A SugarCRM Email is email.
A first sync
Order matters, because links resolve by external_id and a record cannot point at something that is not there yet.
- Call
GET /pingand confirm the key and the company. - Call
GET /schemaand confirm the fields, including your own custom ones. - Post Accounts, in batches of 100.
- Post Contacts, each carrying
account_external_id. - Post Deals, carrying both
account_external_idandcontact_external_id. - Post Activities last, with whichever links apply.
- Check Company Admin → Integrations for what landed.
Then run it on a schedule. Because every record carries its own id, a repeat run updates rather than duplicates. There is nothing to reconcile and no import window to manage.
What this API does not do
- It does not write back into your CRM. Your CRM stays the system of record.
- It does not import Leads. Salespuzzle’s Leads have their own lifecycle; send a qualified lead as a Contact.
- It does not import line items. Send the headline product name on the deal.
- It does not delete. See rule 4.
If you have already imported a CSV. Records brought in through Data Import do not carry the external id this API matches on, so syncing the same records afterwards creates a second copy. Talk to us before your first sync and the external ids can be backfilled onto your existing records. A company starting fresh has nothing to do here.
Questions
For anything not covered here, or a CRM not listed, email info@salespuzzle.co.uk.
