Skip to content
seals.deals

🦭 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.

BASE https://crm.seals.deals/api/v1 AUTH Bearer <api key>

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.

One shape, 28 objects

Same path, same verbs, same envelope, same paging — for every object. Learn contacts and you have learned the other twenty-seven.

The whole product

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.

A key never outranks its author

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.

Deleting is its own permission

delete is a separate scope you opt into. A key with write can change anything it can reach and destroy none of it.

One key per integration

Make as many as you have systems, name them, and revoke one without breaking the rest. Nothing is shared between them.

The API is part of the product, not a tier of it. There is no plan that unlocks it, no per-seat API add-on, and no request budget to buy back.

Quickstart

From zero to your first record in three steps.

Create an API key

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.

Send it as a Bearer token

Add Authorization: Bearer YOUR_API_KEY to every request.

Make a call

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"

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.

read

List and retrieve records. Required for every GET.

write

Create and update records — POST and PATCH.

delete

Permanently delete records via DELETE. Grant deliberately.

Reads and writes are additionally bounded by the permissions of the person who created the key. If they can only see records they own, the key sees only those too — the API never widens access.

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"
    }
  ]
}

Pagination & search

List endpoints take four query parameters.

ParameterTypeDescription
limitintRows to return. Default 50, max 100.
offsetintRows to skip, for paging. Default 0.
qstringCase-insensitive search over the object’s key text fields (name, email, number…).
iduuidReturn 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"

Errors

Standard HTTP status codes; a JSON body with an error message.

StatusMeaningWhen
200OKRead or update succeeded.
201CreatedA record was created.
400Bad requestInvalid JSON, a bad UUID, or a field that failed validation.
401UnauthorizedMissing, malformed, invalid, or revoked API key.
403ForbiddenThe key is missing the scope this call needs.
404Not foundUnknown object, or the object doesn’t support this verb.
413Payload too largeRequest body exceeds the size limit.
429Too many requestsRate limit exceeded — back off and retry.
500Server errorSomething 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.

ObjectReadCreateUpdateDeleteNotes
contactsGETPOSTPATCHDELPeople.
companiesGETPOSTPATCHDELOrganisations.
dealsGETPOSTPATCHDELOpportunities in a pipeline.
productsGETPOSTPATCHDELCatalogue line items.
activitiesGETPOSTPATCHDELTasks, notes, calls, meetings, emails.
quotesGETPOSTPATCHDELNumber auto-assigned on create.
quote_itemsGETPOSTPATCHDELLine items; scoped to their parent quote.
pipelinesGETPOSTPATCHDELDeleting one with deals returns 409.
stagesGETPOSTPATCHDELDeleting one with deals returns 409.
formsGETPOSTPATCHDELToken auto-assigned; delete cascades fields + submissions.
form_fieldsGETPOSTPATCHDELThe questions on a form. Listed by position, ascending.
form_submissionsGETPOSTPATCHDELInbound form submissions.
email_templatesGETPOSTPATCHDELPlain-text or designed. See body_format.
quote_templatesGETPOSTPATCHDELQuote layouts & defaults.
sequencesGETPOSTPATCHDELDelete cascades steps + enrolments.
sequence_stepsGETPOSTPATCHDELOrdered by position, ascending. template_id is an email template.
sequence_enrollmentsGETPOSTPATCHDELPuts a contact into a sequence. Creating an active one starts sending.
automationsGETPOSTPATCHDELTrigger, conditions, actions. Created switched off.
contractsGETPOSTPATCHDELDocuments.
contract_templatesGETPOSTPATCHDELStarting points for a contract.
document_blocksGETPOSTPATCHDELReusable clauses, by category.
content_blocksGETPOSTPATCHDELReusable snippets of copy.
currenciesGETPOSTPATCHDELOrg currencies & FX rates.
custom_propertiesGETPOSTPATCHDELDelete cascades stored values.
emailsGETEvery email sent, with opens and clicks.
quote_emailsGETThe same, for quotes you sent.
automation_runsGETEvery action an automation took, and whether it worked.
automation_enrollmentsGETWhich records an automation is currently working through.
Some objects are read-only, and deliberately so. 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

FieldTypeNotes
first_namestringAt least one of first_name / last_name / email is required.
last_namestring
emailstring
phonestring
job_titlestring
company_iduuidAssociates the contact with a company.
lifecycle_stagestringe.g. lead, customer. Defaults to lead.
lead_statusstringDefaults to new.
owner_iduuidDefaults to the key’s creator.
city, countrystring

deals

FieldTypeNotes
namestringrequired
amountnumber
currencystringISO 4217, e.g. USD.
pipeline_iduuidWhich pipeline the deal lives in.
stage_iduuidCurrent stage. Accepting a linked quote auto-advances this to Closed Won.
company_iduuid
close_datedateYYYY-MM-DD.
prioritystringlow · medium · high.
forecast_categorystring
sourcestring
owner_iduuid

products

FieldTypeNotes
namestringrequired
skustring
unit_pricenumber
currencystringISO 4217.
tax_ratenumberPercent, e.g. 20.
categorystring
activeboolean
image_urlstring

activities

FieldTypeNotes
typestringtask · note · call · meeting · email.
titlestring
bodystring
contact_id / company_id / deal_iduuidWhat the activity is about.
due_datetimestampFor tasks.
statusstringe.g. open, completed.
end_at, location, meeting_urlFor meetings.

quotes & quote_items

FieldTypeNotes
quotes.titlestringNumber is auto-assigned if omitted.
quotes.deal_id / company_id / contact_iduuidAssociations.
quotes.currencystringISO 4217.
quotes.statusstringdraft · sent · accepted · rejected.
quote_items.quote_iduuidrequired parent quote (in your workspace).
quote_items.namestring
quote_items.quantitynumber
quote_items.unit_pricenumber
quote_items.discountnumberPercent.

email_templates

FieldTypeNotes
namestringrequired
subjectstringMay contain {{tokens}}, merged per recipient at send time.
bodystringProse when body_format is text; HTML when it is html.
body_formatstringtext · html. Defaults to text — set it, or a designed body is sent as plain words.
themeobjectThe look of a designed email: colours, web-safe font, width, logo. {} is a complete, valid theme.
owner_iduuidDefaults to the key’s creator.
A designed template is stored as ordinary HTML and styled on the way out — colours and spacing are inlined, and the whole thing is wrapped in the card your theme describes. Send the body as semantic HTML (<p>, <h2>, <ul>) rather than a styled layout of your own.

form_fields

FieldTypeNotes
form_iduuidrequired the form this field belongs to.
labelstringWhat the visitor reads.
field_typestringtext · email · tel · textarea · select · checkbox
positionintOrder on the form, ascending.
required, hiddenboolean
placeholder, help_text, default_valuestring
mapping_kindstringWhere the answer lands: standard (a built-in field), custom, or none.
property_keystringFor standard — e.g. firstName, email.
custom_property_iduuidFor custom — a custom_properties id.
optionsarrayChoices, for the pick-one and pick-many types.
conditionalobjectShow this field only when another answer matches.

sequence_steps

FieldTypeNotes
sequence_iduuidrequired the sequence this step belongs to.
template_iduuidAn email_templates id — not a quote template.
positionintOrder in the sequence, from 0.
delay_daysintDays to wait after the previous step before this one is due.

sequence_enrollments

FieldTypeNotes
sequence_iduuidrequired
contact_iduuidrequired must be a contact the key can edit.
statusstringactive · completed · unenrolled. Only active sends.
current_stepintWhich step is next. Starts at 0.
enrolled_byuuidSet for you — always the key’s creator, whose mailbox the sequence sends from. Ignored if you send it.
!
Enrolling sends real email. An enrolment created with 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

GET/v1/{object}

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"

Retrieve one record

GET/v1/{object}?id={uuid}

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

POST/v1/{object}

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…"
  }'

Update a record

PATCH/v1/{object}?id={uuid}

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…" }'

Delete a record

DELETE/v1/{object}?id={uuid}

Permanently removes the record. Requires the delete scope — keys without it get 403.

!
Deletes are permanent. There’s no undo via the API, and some deletes cascade to children — deleting a form removes its fields and submissions, a sequence its steps and enrolments, a custom property its stored values. A pipeline or stage that still has deals returns 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"
}
Verify every delivery. Compute HMAC-SHA256(rawBody, yourSecret) and compare it, hex-encoded, against the X-Seals-Signature header (after the sha256= prefix) before trusting a payload.