🦭 seals.deals · developer platform
The seals.deals API
A REST API for your CRM — all of it, not the contact list with the machinery held back. Twenty-eight objects behind one endpoint shape, over plain HTTPS, on a key that can never see more than the person who made it.
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, and the machinery behind them: forms and their fields, sequences and their steps, email templates, automations, and the record of everything that was sent and opened. 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.
What’s different here
Five things worth knowing before you start building against it.
Same path, same verbs, same envelope, same paging — for every object. Learn
contacts and you have learned the other twenty-seven.
Not the contact list with the machinery held back. Pipelines and stages, forms and their fields, sequences and their steps, automations and what they did, every email sent and whether it was opened.
Its ceiling is the permission set of whoever created it. If they only see the records they own, so does the key — and its scopes narrow it further from there.
delete is a separate scope you opt into. A key with write can
change anything it can reach and destroy none of it.
Make as many as you have systems, name them, and revoke one without breaking the rest. Nothing is shared between them.
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" }
Fair use
Not metered, not sold by the call — so please be reasonable with it.
There is no request budget attached to your plan, no per-seat allowance, and nothing to buy back
when an import runs long. What we ask instead is that you page with
limit/offset rather than firing many small calls in parallel, and that
scheduled jobs run on a schedule rather than in a loop.
Handle 429 anyway. We may apply a per-key limit if a single integration starts
affecting everyone else’s workspace, and a client that already backs off exponentially will
not notice the day that happens.
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_fields | GET | POST | PATCH | DEL | The questions on a form. Listed by position, ascending. |
| form_submissions | GET | POST | PATCH | DEL | Inbound form submissions. |
| email_templates | GET | POST | PATCH | DEL | Plain-text or designed. See body_format. |
| quote_templates | GET | POST | PATCH | DEL | Quote layouts & defaults. |
| sequences | GET | POST | PATCH | DEL | Delete cascades steps + enrolments. |
| sequence_steps | GET | POST | PATCH | DEL | Ordered by position, ascending. template_id is an email template. |
| sequence_enrollments | GET | POST | PATCH | DEL | Puts a contact into a sequence. Creating an active one starts sending. |
| automations | GET | POST | PATCH | DEL | Trigger, conditions, actions. Created switched off. |
| contracts | GET | POST | PATCH | DEL | Documents. |
| contract_templates | GET | POST | PATCH | DEL | Starting points for a contract. |
| document_blocks | GET | POST | PATCH | DEL | Reusable clauses, by category. |
| content_blocks | GET | POST | PATCH | DEL | Reusable snippets of copy. |
| currencies | GET | POST | PATCH | DEL | Org currencies & FX rates. |
| custom_properties | GET | POST | PATCH | DEL | Delete cascades stored values. |
| emails | GET | — | — | — | Every email sent, with opens and clicks. |
| quote_emails | GET | — | — | — | The same, for quotes you sent. |
| automation_runs | GET | — | — | — | Every action an automation took, and whether it worked. |
| automation_enrollments | GET | — | — | — | Which records an automation is currently working through. |
emails, quote_emails, automation_runs and
automation_enrollments are the record of what actually happened — what was sent,
what was opened, what an automation did. You can read and report on them; nothing can write them
but the system that produced them. A POST, PATCH or DELETE
to one returns 404.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. |
email_templates
| Field | Type | Notes |
|---|---|---|
| name | string | required |
| subject | string | May contain {{tokens}}, merged per recipient at send time. |
| body | string | Prose when body_format is text; HTML when it is html. |
| body_format | string | text · html. Defaults to text — set it, or a designed body is sent as plain words. |
| theme | object | The look of a designed email: colours, web-safe font, width, logo. {} is a complete, valid theme. |
| owner_id | uuid | Defaults to the key’s creator. |
theme describes. Send the body as semantic HTML
(<p>, <h2>, <ul>) rather than a styled
layout of your own.form_fields
| Field | Type | Notes |
|---|---|---|
| form_id | uuid | required the form this field belongs to. |
| label | string | What the visitor reads. |
| field_type | string | text · email · tel · textarea · select · checkbox … |
| position | int | Order on the form, ascending. |
| required, hidden | boolean | |
| placeholder, help_text, default_value | string | |
| mapping_kind | string | Where the answer lands: standard (a built-in field), custom, or none. |
| property_key | string | For standard — e.g. firstName, email. |
| custom_property_id | uuid | For custom — a custom_properties id. |
| options | array | Choices, for the pick-one and pick-many types. |
| conditional | object | Show this field only when another answer matches. |
sequence_steps
| Field | Type | Notes |
|---|---|---|
| sequence_id | uuid | required the sequence this step belongs to. |
| template_id | uuid | An email_templates id — not a quote template. |
| position | int | Order in the sequence, from 0. |
| delay_days | int | Days to wait after the previous step before this one is due. |
sequence_enrollments
| Field | Type | Notes |
|---|---|---|
| sequence_id | uuid | required |
| contact_id | uuid | required must be a contact the key can edit. |
| status | string | active · completed · unenrolled. Only active sends. |
| current_step | int | Which step is next. Starts at 0. |
| enrolled_by | uuid | Set for you — always the key’s creator, whose mailbox the sequence sends from. Ignored if you send it. |
status: "active" is picked up by the sequence runner and mail
goes out to that contact on the schedule the steps describe. Opted-out contacts are always
suppressed, and a step is never sent twice — but there is no undo on a message already sent.
Create with unenrolled first if you are testing.List records
Returns an array of records the key is allowed to see, newest first. Combine with limit,
offset and q.
Objects that are an ordered list rather than a history come back in their own order instead:
form_fields and sequence_steps are returned by position,
ascending, so the first row is the first question on the form and the first step of the sequence.
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.