Developers
The Omnir API
A REST API with JSON in and out, plain record ids and readable field names, plus webhooks when records change. Connect your own tools, or Zapier and Make, in an afternoon.
Quick start
Every Omnir workspace has its own API at its own address. Replace yourcompany with your workspace name:
https://yourcompany.omnir.com/api/v1Create a key in Omnir under My Preferences → API Keys, then list your five most recently changed contacts:
curl https://yourcompany.omnir.com/api/v1/contacts?per_page=5 \
-H "Authorization: Bearer omn_your_key"{
"data": [
{
"id": 4821,
"name": "Jo Rivera",
"first_name": "Jo",
"last_name": "Rivera",
"email": "jo@acme.com",
"organization_id": 1093,
"owner_id": 7,
"do_not_call": false,
"created_at": "2026-10-29T14:02:11Z",
"updated_at": "2026-10-30T09:15:40Z"
}
],
"meta": { "page": 1, "per_page": 5, "total": 1894, "total_pages": 379 }
}GET /api/v1 returns the key's user and every resource the key can use, which is the quickest way to check a new key works.
Authentication
Send your key in the Authorization header on every request. There is no login step and nothing to refresh: a key works until it is revoked.
Authorization: Bearer omn_3f9c1e...- A key acts as its user. It sees and changes exactly what that user can in the CRM: the same roles, sharing rules and field permissions.
- Read-only keys can only make
GETrequests. Use one for reporting and BI tools. - One key per integration, so you can revoke one without breaking the others. Omnir keeps keys encrypted, and you can copy yours again from My Preferences → API Keys at any time.
- Everyone, admins included, creates their own keys in My Preferences. Admins see and revoke every key in the workspace, add webhooks and check usage in Settings → Developer API.
Keep keys on your server. Never put one in a web page or mobile app.
Requests and responses
- Requests and responses are JSON. Send
Content-Type: application/jsonwith a body. - A single record comes back as
{"data": {...}}; a list as{"data": [...], "meta": {...}}. - Records have plain numeric
ids and a read-onlyname: the name the CRM shows for the record. - Links to other records are
…_idfields holding that record's id, such asorganization_id.owner_idis a user or group id. - Dates are
YYYY-MM-DD. Date-times are ISO 8601 and always returned in UTC:2026-11-02T15:30:00Z. You may send any offset, such as2026-11-02T10:30:00-05:00. - Checkboxes are
true/false, numbers are numbers, and multi-select fields are arrays of strings. Empty values arenull.
Resources
Each CRM module is a resource. Custom modules your workspace adds appear too, under their own names. Which ones a key can use depends on its user's permissions.
| Resource | In the CRM |
|---|---|
| contacts | Contacts |
| leads | Leads |
| organizations | Organizations |
| deals | Deals |
| tickets | Help desk tickets |
| tasks | Tasks |
| events | Calls and meetings |
| comments | Comments on any record |
| quotes | Quotes |
| sales_orders | Sales orders |
| invoices | Invoices |
| purchase_orders | Purchase orders |
| products | Products |
| services | Services |
| price_books | Price books |
| vendors | Vendors |
| projects | Projects |
| project_tasks | Project tasks |
| project_milestones | Project milestones |
| documents | Documents and files |
| campaigns | Campaigns |
| faqs | FAQs |
| assets | Assets |
| service_contracts | Service contracts |
GET /api/v1/{resource} list, filter and search
POST /api/v1/{resource} create
GET /api/v1/{resource}/{id} read one
PATCH /api/v1/{resource}/{id} update the fields you send
DELETE /api/v1/{resource}/{id} delete (to the Recycle Bin)
GET /api/v1/{resource}/fields fields, allowed values and relationsFields
Fields have readable names taken from their labels: first_name, lead_source, contract_value. That includes the custom fields your workspace adds. A field's name is fixed the first time the API sees it, so renaming its label in the CRM later never breaks an integration.
GET /api/v1/contacts/fields describes every field: its type, whether it is required or read-only, the allowed values of a picklist, and which resources a link field points to.
{
"data": {
"resource": "contacts",
"fields": [
{ "name": "last_name", "label": "Last Name", "type": "string", "required": true, "read_only": false },
{ "name": "lead_source", "label": "Lead Source", "type": "picklist", "required": false,
"read_only": false, "values": ["Cold Call", "Partner", "Web Site"] },
{ "name": "organization_id", "label": "Organization Name", "type": "reference",
"required": false, "read_only": false, "refers_to": ["organizations"] }
],
"relations": [
{ "name": "deals", "label": "Deals", "can_link": false },
{ "name": "activities", "label": "Activities", "can_link": false },
{ "name": "documents", "label": "Documents", "can_link": true }
]
}
}Tasks and events have starts_at (and events ends_at) as one date-time instead of separate date and time fields. An event's contacts are a list, contact_ids.
Lists, filters and search
| Parameter | What it does |
|---|---|
| page, per_page | Paging. per_page defaults to 20, at most 100. |
| sort | Comma-separated fields; a leading - sorts descending. Default -updated_at. |
| q | Searches the record name, email and phone fields. |
| fields | Return only these fields, e.g. fields=id,name,email. |
| updated_since | Only records changed at or after this UTC time. Use it to sync. |
| {field}=value | Filter on any field: email=jo@acme.com. |
| {field}[op]=value | Filter with an operator: amount[gte]=1000. |
| Operator | Meaning |
|---|---|
| eq | Equals (the default: field=value) |
| ne | Does not equal |
| gt, gte | Greater than, or equal to |
| lt, lte | Less than, or equal to |
| contains | Contains the text |
| starts_with, ends_with | Starts or ends with the text |
| in, not_in | One of a comma-separated list, or none of it |
| empty | empty=true for blank values, empty=false for filled ones |
# Open deals worth 10,000 or more, biggest first
curl -g "https://yourcompany.omnir.com/api/v1/deals?amount[gte]=10000&sales_stage[not_in]=Closed%20Won,Closed%20Lost&sort=-amount" \
-H "Authorization: Bearer omn_your_key"
# Everything that changed since your last sync
curl "https://yourcompany.omnir.com/api/v1/contacts?updated_since=2026-10-30T00:00:00Z&per_page=100&page=1" \
-H "Authorization: Bearer omn_your_key"With curl, add -g so the square brackets reach the API instead of being read as a curl pattern. HTTP libraries send them as they are.
Filters take the same values you would write: ids for link fields, true/false for checkboxes, UTC date-times for date-time fields. An unknown field, operator or sort is answered with a 400 that names it.
GET /api/v1/search?q=acme searches contacts, leads, organizations, deals, tickets, projects, quotes and invoices at once; name others with resources=products,vendors.
Creating and updating
curl -X POST https://yourcompany.omnir.com/api/v1/contacts \
-H "Authorization: Bearer omn_your_key" \
-H "Content-Type: application/json" \
-d '{"first_name": "Jo", "last_name": "Rivera", "email": "jo@acme.com",
"organization_id": 1093, "lead_source": "Partner"}'POSTanswers201with the new record, including its id and the values Omnir filled in.PATCHchanges only the fields you send and answers with the whole updated record.- A new record is owned by the key's user unless you send
owner_id. - An event without
ends_atgets the user's default call or meeting length. - Unknown fields are rejected, so a typo never fails silently. Read-only fields are ignored, so you can send back a record you read.
- Saves run the CRM's workflows, assignment rules and history, exactly as a save in the app does.
Every problem in a request is reported at once:
{
"error": {
"code": "validation_failed",
"message": "Some fields are not valid.",
"details": [
{ "field": "email", "message": "Expected an email address." },
{ "field": "lead_source", "message": "\"Web\" is not one of the allowed values: Cold Call, Partner, Web Site." },
{ "field": "last_name", "message": "This field is required." }
]
}
}Files
Upload a file as multipart/form-data with the file in a field called file. Posted to a record's documents, it is stored and linked in one step:
curl -X POST https://yourcompany.omnir.com/api/v1/contacts/4821/documents \
-H "Authorization: Bearer omn_your_key" \
-F "file=@proposal.pdf" \
-F "title=Proposal, November"
curl https://yourcompany.omnir.com/api/v1/documents/77/file \
-H "Authorization: Bearer omn_your_key" -o proposal.pdfUsers and groups
GET /api/v1/me is the key's user. GET /api/v1/users lists active users (status=all includes inactive ones) and GET /api/v1/groups the groups, so you can resolve and choose an owner_id. Both are read-only.
Errors
Errors use HTTP status codes and always have the same body: {"error": {"code", "message", "details"}}. Branch on code; the message is written for people and may change.
| Status | When |
|---|---|
| 200 | The request worked. |
| 201 | A record was created or linked. |
| 204 | Deleted or unlinked; no body. |
| 400 | The request is malformed: bad JSON, an unknown parameter or operator. |
| 401 | The key is missing, wrong or revoked. |
| 403 | The key may not do this: a read-only key, or a record its user may not change. |
| 404 | No such resource or record, or one its user cannot see. |
| 405 | That method is not supported on this URL. |
| 422 | The body has invalid or missing fields; details lists each one. |
| 429 | Over the rate limit; wait for Retry-After seconds. |
| 500 | Our fault. Retry later, and tell us if it persists. |
| Code | Meaning |
|---|---|
| unauthenticated | No API key was sent. |
| invalid_key | The key does not exist or was revoked. |
| read_only_key | A read-only key tried to change data. |
| forbidden | The key's user is not allowed to do this. |
| not_found | The resource or record does not exist, or is hidden from this user. |
| method_not_allowed | Use one of the methods named in the message. |
| invalid_json | The body is not a JSON object. |
| invalid_parameters | A query parameter, filter or sort is not valid; see details. |
| validation_failed | Fields in the body are not valid; see details. |
| not_linkable | This relation is a field on the other record; PATCH that record. |
| rate_limited | Too many requests this minute. |
| server_error | Something went wrong on our side. |
Rate limits
Each key may make 120 requests a minute. Every response carries the limit, what is left and when the window resets:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1793340300Over the limit the API answers 429 with a Retry-After header in seconds. For bulk syncs, ask for 100 records a page and use updated_since rather than reading everything again.
Admins can see the last 30 days of requests, per key, under Settings → Developer API → Usage.
Webhooks
Instead of asking Omnir for changes, let Omnir tell you. An admin adds a webhook in Settings → Developer API → Webhooks with a URL and the changes to send: created, updated or deleted, for all records or chosen ones. Omnir then POSTs JSON to your URL as they happen.
{
"id": "evt_d21f716df6b1f99e",
"event": "contacts.updated",
"created_at": "2026-10-30T05:42:45Z",
"data": { "id": 4821, "name": "Jo Rivera", "title": "Head of Ops", "...": "..." },
"changed_fields": ["title"]
}- Events are
{resource}.created,.updatedand.deleted, such asdeals.created. datais the whole record, in the same shape the API returns. A deleted record sends only its id.- Updates list the
changed_fields; a save that changes nothing sends nothing. - Headers:
X-Omnir-Event,X-Omnir-Delivery(unique per delivery) andX-Omnir-Signature. - The
idis the same for every retry of an event, so you can ignore one you have already handled.
Check the signature
Every webhook has a signing secret, shown in its settings. The signature header is t=<unix time>,v1=<signature>, where the signature is the hex HMAC-SHA256 of <t>.<raw body> with that secret. Compare it before trusting the body, and reject old timestamps:
import crypto from 'node:crypto'
export function verifyOmnir(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
const expected = crypto
.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex')
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < toleranceSeconds
return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}function verify_omnir(string $rawBody, string $header, string $secret, int $tolerance = 300): bool {
parse_str(str_replace(',', '&', $header), $parts);
$expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
return abs(time() - (int) $parts['t']) < $tolerance && hash_equals($expected, $parts['v1']);
}Answer quickly; Omnir retries
Answer with any 2xx status within 10 seconds and do slow work afterwards. Anything else, or no answer, is retried:
| Attempt | Sent |
|---|---|
| 1 | At once |
| 2 | 1 minute later |
| 3 | 5 minutes later |
| 4 | 30 minutes later |
| 5 | 2 hours later |
| 6 | 12 hours later |
| 7 | 24 hours later, then the delivery is marked failed |
From the same Webhooks tab, admins can see each webhook's recent deliveries, send a test, pause it or rotate its secret. Webhooks are only sent to public https:// or http:// addresses.
Your workspace's reference
Because every workspace has its own fields and custom modules, each one has its own complete reference too, as an OpenAPI 3 file. Fetch it with your key and open it in Postman, Insomnia or a code generator:
curl https://yourcompany.omnir.com/api/v1/openapi.json \
-H "Authorization: Bearer omn_your_key" -o omnir-openapi.jsonThis is version 1. Changes within /v1 only add things: new resources, fields or optional parameters. Anything that would break an integration gets a new version.
Building something?
Tell us what you are connecting and we will help you get it right, or build the connector if it is something many customers need.