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. New ones are created only by running an automation.
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. The one way to cause an enrolment is the run an automation action, which goes through the same guards the engine applies to a triggered enrolment.

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.

Records in the recycle bin are left out — someone deleted them in the app, and they can be restored there for 90 days. So are a binned quote’s items and emails, and activities whose linked records are all in the bin.

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. A record in the recycle bin comes back as zero elements, the same as an id that does not exist.

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. A link to a record in the recycle bin — a contact_id of a deleted contact, say — is refused with 400; restore the record first.

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. A record in the recycle bin returns 404, and a link to one returns 400.

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}

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

Contacts, companies, deals and quotes go to the recycle bin, exactly as a delete in the app does: gone from the API at once, restorable in the app for 90 days, then purged. The response says which happened — {"deleted": true, "id": "…", "recycle_bin": true, "restorable_until": "…"}.

!
Everything else is deleted permanently. There’s no undo for those, 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 record already in the recycle bin returns 404. 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…" } }

Run an automation

POST/v1/automations/{id}/enroll

Enrols records in an enabled automation right now, by hand — the only way an automation whose trigger is “only when someone runs it by hand” ever runs, and a way to (re)run any other automation on records you choose. Requires the write scope.

The body names the records, or asks for every record that currently matches the automation’s conditions:

FieldTypeMeaning
record_idsuuid[]Up to 200 ids of the automation’s object type (contacts, companies, deals or quotes). Duplicates are ignored.
allbooleantrue = every record that matches the automation’s conditions right now, never-enrolled records first, up to 500 per call. Same as "record_ids": null. Must be explicit — an empty body is a 400, never a broad run by accident.

Every record goes through the same guards a triggered enrolment gets: the automation’s conditions, its re-enrolment rule, one live run per record, and at most 5 enrolments per record per day. Records outside the key creator’s edit scope are skipped, not enrolled. Instant steps (set a property, create a task, notify) happen inside the call; emails and delayed steps are queued and go out with the next scheduled run, usually within 15 minutes.

Response fieldMeaning
requestedRecords the call looked at, after de-duplication and the cap.
enrolledRecords that were enrolled and started.
skippedRecords that were not, with the breakdown in reasons: not_found, no_access, filters, already_enrolled, active, cap.
matching, truncatedOnly with "all": true: how many records match right now, and whether the per-call cap left some for another call. Call again to continue — it picks up never-enrolled records first.
Status codes. 404 when the automation doesn’t exist in your workspace, 409 when it is switched off (enable it first), 403 when the key’s creator has no edit access to that kind of record. Every enrolment is written to automation_enrollments with enrolled_by set to the key’s creator, and shows up in automation_runs as an enroll row.
# Two specific contacts
curl -X POST "https://crm.seals.deals/api/v1/automations/161f…/enroll" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"record_ids":["b1e2…","c3d4…"]}'

# → 200 { "object": "automations", "data": { "automation_id": "161f…", "requested": 2, "enrolled": 1, "skipped": 1,
#          "reasons": { "not_found": 0, "no_access": 0, "filters": 1, "already_enrolled": 0, "active": 0, "cap": 0 } } }

# Every record that matches the automation's conditions
curl -X POST "https://crm.seals.deals/api/v1/automations/161f…/enroll" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"all":true}'

# → 200 { "object": "automations", "data": { …, "matching": 5, "requested": 5, "enrolled": 5, "skipped": 0, "truncated": false } }

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

Records that go to the recycle bin follow one rule. Going into it — deleted in the app or with DELETE — sends <object>.deleted; restoring it sends <object>.updated with the whole record. Nothing is sent while it sits in the bin, or when it is purged 90 days later. An activity whose linked records are all in the bin goes, and comes back, with them.

{
  "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.