Integrate from your backend
Your backend knows who signed up, which account they belong to, which plan that account is on and which deals are open. This page is the set of calls that keeps Pathbound in step with it. Each record is written through its own route and keyed on your own id, and sending the same request twice changes nothing, so you can send it again whenever you are not sure the first one arrived.
| Something happens | Calls |
|---|---|
| A user registers | POST /v1/companies for their account, then POST /v1/contacts naming it |
| An account changes plan | POST /v1/companies again, with the new values |
| A user joins a team account | POST /v1/contacts naming that account |
| A deal is opened or moves | POST /v1/deals with your deal id |
| You load what you already have | The bulk forms of the same calls |
Before you start
Section titled “Before you start”- An API key from Settings → API keys with the scopes
contacts:write,companies:writeand, for deals,deals:write. See Authentication. - Your own ids: one for each account, one for each user, one for each deal. They should be stable and opaque. See Choose a safe external ID.
A registration
Section titled “A registration”First the account, keyed on your account id:
curl -X POST https://api.pathbound.ai/v1/companies \ -H "Authorization: Bearer $PATHBOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "external_id": "acct_51c0e7", "domain": "northwind.example", "properties": { "name": "Northwind", "plan": "trial", "seats": 5 } }'Then the user, keyed on your user id, naming the account by the same account id:
curl -X POST https://api.pathbound.ai/v1/contacts \ -H "Authorization: Bearer $PATHBOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "external_id": "usr_8f3a2c9e", "email": "[email protected]", "properties": { "firstname": "Ada", "lastname": "Lovelace", "registered_at": "2026-10-22T09:00:00Z" }, "company": { "external_id": "acct_51c0e7" } }'{ "status": "success", "message": "Contact created successfully", "contact_id": "665f1c2ab3d4e5f6012345ab", "created": true, "contact": { "…": "the contact record" }, "timestamp": "2026-10-22T09:00:01.000Z"}- Each call is
201with"created": truewhen it creates the record and200with"created": falsewhen it finds it. - A record is found by your id; else it is one with no id yet that holds the domain or the email (a company or a contact your CRM brought in, say), which takes your id; else it is created.
- A user who signs up with a personal address (
gmail.com) still gets an account: a free email provider’s domain is left off the company, which is written all the same. companyonly looks the account up. Write the account first: a user naming an account that does not exist is refused with422.
Create a Company and Create a Contact have every field and every case.
In the browser
Section titled “In the browser”If the events snippet is on your app, identify the user there when they sign in, with the same user id:
window.pathbound = window.pathbound || [];window.pathbound.push(['identify', user.pathboundExternalId, { email: user.email }]);That ties what they do in the browser, before and after signing up, to the contact your backend wrote. It writes no account and changes no property of an existing contact. See Identify logged-in users.
A plan change
Section titled “A plan change”Send the account call again with the new values. Only what you send changes:
curl -X POST https://api.pathbound.ai/v1/companies \ -H "Authorization: Bearer $PATHBOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "external_id": "acct_51c0e7", "properties": { "plan": "pro", "seats": 20 } }'A change to a user is the contact call again in the same way.
A team account
Section titled “A team account”Every member’s contact names the same account id, and every member is linked to that one company:
curl -X POST https://api.pathbound.ai/v1/contacts \ -H "Authorization: Bearer $PATHBOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "external_id": "usr_2b9d41", "email": "[email protected]", "company": { "external_id": "acct_51c0e7" } }'A user who moves to another account is sent with the new account id: they are linked to it and no longer listed under the old one.
A deal
Section titled “A deal”Send your deal id and link the deal by the same ids you used above. A deal that already has the id is updated, so the same call opens the deal and moves it later:
curl -X POST https://api.pathbound.ai/v1/deals \ -H "Authorization: Bearer $PATHBOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "external_deal_id": "deal_7731", "properties": { "name": "Northwind annual", "stage": "proposal", "amount": 24000 }, "contact": { "external_id": "usr_8f3a2c9e" }, "company": { "external_id": "acct_51c0e7" } }'Like company on a contact, contact and company here only look records up: a key that finds nothing is a 422. See Create a Deal.
A backfill
Section titled “A backfill”To load the accounts, users and deals you already have, send the same items through POST /v1/companies/bulk, then POST /v1/contacts/bulk, then POST /v1/deals/bulk, up to 1,000 items a request. An item keyed on your id is created or updated, and reported created or updated; a bad item is reported on its own and the others go through. Sending a batch again is safe.
Reading back by your own ids
Section titled “Reading back by your own ids”| Record | Call |
|---|---|
| An account | GET /v1/companies?external_id=acct_51c0e7 |
| A user | GET /v1/contacts?external_id=usr_8f3a2c9e |
| A deal | GET /v1/deals?external_deal_id=deal_7731 |
Each returns zero or one record. Reads are metered.
What to retry
Section titled “What to retry”Every call on this page can be sent again with the same body, so retry it when you do not know whether it arrived:
- a timeout or a dropped connection;
- a
5xx; - a
429whoseresourceisv1_api_requests_per_min, after the number of seconds in theRetry-Afterheader.
Do not send the same body again after these. Change the request, or what it depends on:
| Answer | What it means |
|---|---|
400 | A field is malformed. code, details or error says which. |
403 | The key lacks a scope. |
409 from a contact, code: "EXTERNAL_ID_CONFLICT" | The email belongs to a contact keyed to another of your ids. contact_id is that contact. |
422, code: "COMPANY_NOT_FOUND" or "CONTACT_NOT_FOUND" | The account or the user a key names is not there yet: write it, then send the call again. |
429 whose resource is contacts, companies or deals | The workspace’s stored records of that kind reached their limit. Waiting does not lift it. |
skipped_traits in a successful answer tells you a field was left out and why, and needs no retry: an email another contact holds, or a domain that is a free email provider’s, is no host name, or belongs to another company. The rest of the record was written.