🦭 seals.deals · developer platform
The seals.deals API
A REST API for your CRM. Read and write contacts, companies, deals, quotes and more over plain HTTPS — scoped to a key you control, in your own workspace.
Introduction
Everything you can see in seals.deals, your integrations can reach over HTTP.
The API is organised around the objects you already work with — contacts, companies, deals, products, activities and quotes. It speaks JSON, uses standard HTTP verbs, and returns predictable status codes. Every request runs inside the workspace that issued the key and is bounded by that key’s permissions, so a key can never see or touch more than its creator can.
- Base URL — all endpoints live under
https://crm.seals.deals/api/v1. - JSON everywhere — send
Content-Type: application/json; every response is JSON. - Envelope — responses are wrapped as
{ "object": "…", "data": … }. - Identifiers are UUIDs; timestamps are ISO 8601 (UTC); money uses ISO 4217 currency codes.
Quickstart
From zero to your first record in three steps.
In seals.deals go to Settings → API keys, create a key, and pick its
scopes (read, write, delete). Copy the key — it’s shown once.
Add Authorization: Bearer YOUR_API_KEY to every request.
List your contacts to confirm it works, then start creating and updating records.
curl https://crm.seals.deals/api/v1/contacts?limit=3 \
-H "Authorization: Bearer YOUR_API_KEY"
const res = await fetch("https://crm.seals.deals/api/v1/contacts?limit=3", {
headers: { "Authorization": `Bearer ${process.env.SEALS_API_KEY}` },
});
const { data } = await res.json();
console.log(data);
import requests
r = requests.get(
"https://crm.seals.deals/api/v1/contacts",
headers={"Authorization": f"Bearer {API_KEY}"},
params={"limit": 3},
)
r.raise_for_status()
contacts = r.json()["data"]
Authentication
One bearer token per integration, scoped and revocable.
Every request must carry an API key as a bearer token:
Authorization: Bearer YOUR_API_KEY
Keys are created in Settings → API keys and hashed at rest — we never store the raw
key, so copy it when it’s shown. Requests without a valid, unrevoked key get 401.
Scopes
A key carries one or more scopes. A request that needs a scope the key lacks returns 403.
List and retrieve records. Required for every GET.
Create and update records — POST and PATCH.
Permanently delete records via DELETE. Grant deliberately.
Requests & responses
Predictable shapes in, predictable shapes out.
Send JSON bodies with Content-Type: application/json. Successful responses are wrapped
in an envelope naming the object and carrying the payload in data — an array for lists,
a single object for one record.
{
"object": "contacts",
"data": [
{
"id": "b1e2c3d4-5f6a-4b8c-9d0e-1f2a3b4c5d6e",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@analytical.co",
"lifecycle_stage": "lead",
"created_at": "2026-07-09T08:15:00Z"
}
]
}
{
"object": "deals",
"data": {
"id": "f0b85efc-4e15-4d1d-93c0-ca8bb5450889",
"name": "VEEWER Premium — Acme",
"amount": 12000,
"currency": "USD",
"stage_id": "248c5509-b5a6-40a8-ad9e-020ac722989b"
}
}
Pagination & search
List endpoints take four query parameters.
| Parameter | Type | Description |
|---|---|---|
| limit | int | Rows to return. Default 50, max 100. |
| offset | int | Rows to skip, for paging. Default 0. |
| q | string | Case-insensitive search over the object’s key text fields (name, email, number…). |
| id | uuid | Return a single record by id (same as the Retrieve endpoint). |
# Page 2 of companies matching "acme", 25 per page
curl "https://crm.seals.deals/api/v1/companies?q=acme&limit=25&offset=25" \
-H "Authorization: Bearer YOUR_API_KEY"
const params = new URLSearchParams({ q: "acme", limit: "25", offset: "25" });
const res = await fetch(`https://crm.seals.deals/api/v1/companies?${params}`, {
headers: { "Authorization": `Bearer ${key}` },
});
Errors
Standard HTTP status codes; a JSON body with an error message.
| Status | Meaning | When |
|---|---|---|
| 200 | OK | Read or update succeeded. |
| 201 | Created | A record was created. |
| 400 | Bad request | Invalid JSON, a bad UUID, or a field that failed validation. |
| 401 | Unauthorized | Missing, malformed, invalid, or revoked API key. |
| 403 | Forbidden | The key is missing the scope this call needs. |
| 404 | Not found | Unknown object, or the object doesn’t support this verb. |
| 413 | Payload too large | Request body exceeds the size limit. |
| 429 | Too many requests | Rate limit exceeded — back off and retry. |
| 500 | Server error | Something went wrong on our side. |
{ "error": "API key is missing the delete scope" }
Rate limits
Keep it reasonable; handle 429 gracefully.
Requests are rate-limited per key. If you exceed the limit you’ll get a 429 — wait a moment
and retry with exponential backoff. Batch your work with limit/offset paging rather than
firing many small calls in parallel.
Objects
What you can read, create, update, and delete.
| Object | Read | Create | Update | Delete | Notes |
|---|---|---|---|---|---|
| contacts | GET | POST | PATCH | DEL | People. |
| companies | GET | POST | PATCH | DEL | Organisations. |
| deals | GET | POST | PATCH | DEL | Opportunities in a pipeline. |
| products | GET | POST | PATCH | DEL | Catalogue line items. |
| activities | GET | POST | PATCH | DEL | Tasks, notes, calls, meetings, emails. |
| quotes | GET | POST | PATCH | DEL | Number auto-assigned on create. |
| quote_items | GET | POST | PATCH | DEL | Line items; scoped to their parent quote. |
| pipelines | GET | POST | PATCH | DEL | Deleting one with deals returns 409. |
| stages | GET | POST | PATCH | DEL | Deleting one with deals returns 409. |
| forms | GET | POST | PATCH | DEL | Token auto-assigned; delete cascades fields + submissions. |
| form_submissions | GET | POST | PATCH | DEL | Inbound form submissions. |
| email_templates | GET | POST | PATCH | DEL | Reusable email templates. |
| quote_templates | GET | POST | PATCH | DEL | Quote layouts & defaults. |
| sequences | GET | POST | PATCH | DEL | Delete cascades steps + enrolments. |
| contracts | GET | POST | PATCH | DEL | Documents. |
| currencies | GET | POST | PATCH | DEL | Org currencies & FX rates. |
| custom_properties | GET | POST | PATCH | DEL | Delete cascades stored values. |
Field reference
Writable fields for the create/update objects. Related ids (company_id,
owner_id…) must belong to your workspace.
contacts
| Field | Type | Notes |
|---|---|---|
| first_name | string | At least one of first_name / last_name / email is required. |
| last_name | string | |
| string | ||
| phone | string | |
| job_title | string | |
| company_id | uuid | Associates the contact with a company. |
| lifecycle_stage | string | e.g. lead, customer. Defaults to lead. |
| lead_status | string | Defaults to new. |
| owner_id | uuid | Defaults to the key’s creator. |
| city, country | string |
deals
| Field | Type | Notes |
|---|---|---|
| name | string | required |
| amount | number | |
| currency | string | ISO 4217, e.g. USD. |
| pipeline_id | uuid | Which pipeline the deal lives in. |
| stage_id | uuid | Current stage. Accepting a linked quote auto-advances this to Closed Won. |
| company_id | uuid | |
| close_date | date | YYYY-MM-DD. |
| priority | string | low · medium · high. |
| forecast_category | string | |
| source | string | |
| owner_id | uuid |
products
| Field | Type | Notes |
|---|---|---|
| name | string | required |
| sku | string | |
| unit_price | number | |
| currency | string | ISO 4217. |
| tax_rate | number | Percent, e.g. 20. |
| category | string | |
| active | boolean | |
| image_url | string |
activities
| Field | Type | Notes |
|---|---|---|
| type | string | task · note · call · meeting · email. |
| title | string | |
| body | string | |
| contact_id / company_id / deal_id | uuid | What the activity is about. |
| due_date | timestamp | For tasks. |
| status | string | e.g. open, completed. |
| end_at, location, meeting_url | — | For meetings. |
quotes & quote_items
| Field | Type | Notes |
|---|---|---|
| quotes.title | string | Number is auto-assigned if omitted. |
| quotes.deal_id / company_id / contact_id | uuid | Associations. |
| quotes.currency | string | ISO 4217. |
| quotes.status | string | draft · sent · accepted · rejected. |
| quote_items.quote_id | uuid | required parent quote (in your workspace). |
| quote_items.name | string | |
| quote_items.quantity | number | |
| quote_items.unit_price | number | |
| quote_items.discount | number | Percent. |
List records
Returns an array of records the key is allowed to see, newest first. Combine with limit,
offset and q.
curl "https://crm.seals.deals/api/v1/deals?limit=20" \
-H "Authorization: Bearer YOUR_API_KEY"
const res = await fetch("https://crm.seals.deals/api/v1/deals?limit=20", {
headers: { "Authorization": `Bearer ${key}` },
});
const { data: deals } = await res.json();
r = requests.get(
"https://crm.seals.deals/api/v1/deals",
headers={"Authorization": f"Bearer {API_KEY}"},
params={"limit": 20},
)
deals = r.json()["data"]
Retrieve one record
Fetch a single record by id. Returns data as an array with zero or one element.
curl "https://crm.seals.deals/api/v1/companies?id=3f2a…" \
-H "Authorization: Bearer YOUR_API_KEY"
Create a record
Send a JSON object of writable fields. Returns 201 with the created record. Requires the
write scope.
curl -X POST https://crm.seals.deals/api/v1/deals \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "VEEWER Premium — Acme",
"amount": 12000,
"currency": "USD",
"company_id": "3f2a…",
"pipeline_id": "c74d…",
"stage_id": "248c…"
}'
const res = await fetch("https://crm.seals.deals/api/v1/deals", {
method: "POST",
headers: {
"Authorization": `Bearer ${key}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "VEEWER Premium — Acme",
amount: 12000,
currency: "USD",
company_id: companyId,
pipeline_id: pipelineId,
stage_id: stageId,
}),
});
const { data: deal } = await res.json();
r = requests.post(
"https://crm.seals.deals/api/v1/deals",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"name": "VEEWER Premium — Acme",
"amount": 12000,
"currency": "USD",
"company_id": company_id,
},
)
deal = r.json()["data"]
Update a record
Partial update — send only the fields you want to change; everything else is left untouched.
Requires the write scope.
curl -X PATCH "https://crm.seals.deals/api/v1/deals?id=f0b8…" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "amount": 15000, "stage_id": "9a1d…" }'
await fetch(`https://crm.seals.deals/api/v1/deals?id=${dealId}`, {
method: "PATCH",
headers: {
"Authorization": `Bearer ${key}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ amount: 15000, stage_id: wonStageId }),
});
Delete a record
Permanently removes the record. Requires the delete scope — keys without it get 403.
409 (reassign them first). Give the delete scope only to keys that truly need it.curl -X DELETE "https://crm.seals.deals/api/v1/contacts?id=b1e2…" \
-H "Authorization: Bearer YOUR_API_KEY"
# → 200 { "object": "contacts", "data": { "deleted": true, "id": "b1e2…" } }
Webhooks
Get notified when records change, instead of polling.
Register an endpoint under Settings → API & webhooks and subscribe to events. When a
record is created, updated, or deleted, seals.deals sends a signed POST to your URL — so
your systems stay in sync in real time without polling.
Events are <object>.created, <object>.updated, and
<object>.deleted (e.g. deal.updated, contact.created).
{
"id": "d3f1…", // unique delivery id
"event": "deal.updated",
"data": { /* the full record */ },
"sent_at": "2026-07-10T09:00:00Z"
}
X-Seals-Event: deal.updated
X-Seals-Delivery: d3f1… # matches payload.id
X-Seals-Timestamp: 1783934400
X-Seals-Signature: sha256=<hex> # HMAC-SHA256 of the raw body,
# keyed with your webhook secret
HMAC-SHA256(rawBody, yourSecret) and compare it, hex-encoded, against the
X-Seals-Signature header (after the sha256= prefix) before trusting a payload.