Skip to main content
DeveloperAPI Reference

Contacts API

CRUD, bulk, and field mapping operations on NimbusOS contacts via REST API, with examples for create, update, search, and delete.

9 min read
Updated April 23, 2026
2,050 words

The Contacts API is the primary way to read, write, search, and bulk-manage contacts in NimbusOS programmatically. Every endpoint enforces workspace isolation through the TenantScopedMixin, returns JSON, and supports cursor-based pagination for large result sets. This article covers the core endpoints with example requests and responses.

Base URL and Authentication

All endpoints live under https://getnimbusos.com/api/v1/contacts/.

Authentication via Bearer token in the Authorization header. JWT or API key. See the Authentication article.

Contact Object

A contact is returned as:

{
  "id": "cont_abc123",
  "workspace_id": "ws_xyz789",
  "email": "alice@acme.com",
  "first_name": "Alice",
  "last_name": "Smith",
  "full_name": "Alice Smith",
  "title": "Director of Revenue Operations",
  "seniority": "director",
  "department": "revenue_operations",
  "phone": "+1-555-0100",
  "mobile": null,
  "linkedin_url": "https://linkedin.com/in/alicesmith",
  "city": "New York",
  "state": "NY",
  "country": "US",
  "time_zone": "America/New_York",
  "lead_source": "apollo_import_20260401",
  "lead_score": 72,
  "icp_tier": "A",
  "grade": "A",
  "tags": ["director_plus", "us_east", "verified_email"],
  "verification_status": "valid",
  "is_enriched": true,
  "is_deleted": false,
  "company": {
    "id": "comp_def456",
    "name": "Acme Corporation",
    "domain": "acme.com",
    "industry": "Software",
    "employee_count": 350,
    "revenue_band": "25M_50M"
  },
  "custom_fields": {
    "onboarding_stage": "trial_active",
    "assigned_rep": "rep_1"
  },
  "created_at": "2026-04-01T10:00:00Z",
  "updated_at": "2026-04-20T14:32:11Z"
}

List Contacts

GET /api/v1/contacts/?page_size=100&icp_tier=A,B
Authorization: Bearer <token>

Query parameters:

  • page_size: 1 to 500, default 50.
  • cursor: pagination cursor from previous response.
  • icp_tier: comma-separated list of tiers (A, B, C, D).
  • grade: comma-separated list of grades.
  • tag: filter by one or more tags.
  • segment_id: filter to segment members.
  • search: full-text search across email, name, title, company.
  • created_after, updated_after: ISO 8601 timestamps.
  • sort: one of created_desc, created_asc, updated_desc, lead_score_desc.

Response:

{
  "data": [
    { "id": "cont_abc", ... },
    ...
  ],
  "meta": {
    "next_cursor": "eyJpZCI6ImNvbnRfMTIzIn0=",
    "has_more": true,
    "total_count": 1250
  }
}

Pagination is cursor-based. Pass next_cursor in the next request to continue.

Get Contact

GET /api/v1/contacts/cont_abc123/

Returns the single contact object.

Create Contact

POST /api/v1/contacts/
Content-Type: application/json

{
  "email": "bob@example.com",
  "first_name": "Bob",
  "last_name": "Johnson",
  "title": "VP Marketing",
  "company_domain": "example.com",
  "tags": ["inbound_lead", "demo_requested"],
  "custom_fields": {
    "lead_source_detail": "organic_search_q2"
  }
}

Response: 201 Created with the created contact object.

Creation runs through the same pipeline as import: dedup against GlobalContact, suppression check, ICP segmentation, scoring. If dedup finds an existing contact in the workspace, the request returns 200 with the existing contact (not 201).

If the email is currently assigned to another client (agency mode) or is globally suppressed, the response is 409 with a specific error code.

Update Contact

PATCH /api/v1/contacts/cont_abc123/
Content-Type: application/json

{
  "title": "Senior Director of RevOps",
  "tags": ["director_plus", "us_east", "verified_email", "executive_sponsor"]
}

PATCH updates only the provided fields. Omitted fields remain unchanged. tags replaces the full array; to add a tag without replacing, use the tags endpoint:

POST /api/v1/contacts/cont_abc123/tags/
Content-Type: application/json

{
  "add": ["executive_sponsor"],
  "remove": []
}

Delete Contact

Soft delete:

DELETE /api/v1/contacts/cont_abc123/
Content-Type: application/json

{
  "deletion_reason": "opted_out_via_form"
}

Returns 200. The contact is marked is_deleted=true but preserved in the database.

Hard delete (GDPR):

DELETE /api/v1/contacts/cont_abc123/?hard=true

Requires the admin:gdpr scope. Removes the contact row, clears engagement history, and writes a retention log.

Bulk Create

POST /api/v1/contacts/bulk/
Content-Type: application/json

{
  "contacts": [
    { "email": "contact1@example.com", "first_name": "Alice", ... },
    { "email": "contact2@example.com", "first_name": "Bob", ... },
    ...
  ],
  "options": {
    "dedup_behavior": "update",
    "trigger_enrichment": true,
    "trigger_scoring": true
  }
}

Up to 1,000 contacts per request. For larger batches, use the import job endpoint:

POST /api/v1/imports/
Content-Type: multipart/form-data

file: <csv>
field_mapping_id: fm_abc

The import endpoint supports up to 500,000 rows per upload and runs asynchronously.

Beyond the basic search query param, the API supports advanced query via POST:

POST /api/v1/contacts/search/
Content-Type: application/json

{
  "filters": [
    { "field": "icp_tier", "op": "in", "value": ["A", "B"] },
    { "field": "lead_score", "op": "gte", "value": 60 },
    { "field": "has_tag", "op": "eq", "value": "verified_email" }
  ],
  "filters_logic": "AND",
  "sort": [{ "field": "lead_score", "direction": "desc" }],
  "page_size": 100
}

Supported operators: eq, ne, gt, gte, lt, lte, in, not_in, contains, starts_with, ends_with, is_empty, is_not_empty.

Custom Fields

Custom fields are workspace-scoped. Create via:

POST /api/v1/custom-fields/
Content-Type: application/json

{
  "name": "onboarding_stage",
  "display_label": "Onboarding Stage",
  "field_type": "dropdown",
  "options": ["not_started", "trial_active", "trial_expired", "customer"],
  "fallback_value": "not_started"
}

Field types: string, number, boolean, date, dropdown, multi_select.

Once a custom field exists, contact create and update calls can include custom_fields.<name> with the value.

Engagement Events

Each contact has a timeline of events. Read:

GET /api/v1/contacts/cont_abc123/events/?event_type=email_opened,email_replied&limit=50

Events are immutable. Writing events directly via API is not supported; events are created by the email engine and the reply intelligence layer.

Enrollments

List a contact's sequence enrollments:

GET /api/v1/contacts/cont_abc123/enrollments/

Enroll a contact:

POST /api/v1/sequences/seq_abc/enroll/
Content-Type: application/json

{
  "contact_ids": ["cont_abc123", "cont_def456"]
}

The enrollment runs through the campaign filter and ICP checks the way a normal enrollment does.

Error Responses

Standard error shape:

{
  "error": {
    "code": "validation_error",
    "message": "Email is required",
    "field": "email"
  }
}

Common error codes:

  • 400 validation_error: request body invalid
  • 401 authentication_required: no valid auth
  • 403 forbidden: scope insufficient or tenant mismatch
  • 404 not_found: resource does not exist in this workspace
  • 409 conflict: dedup or assignment conflict
  • 422 suppressed: contact is suppressed
  • 429 rate_limited: too many requests
  • 500 internal_error: server-side issue

Idempotency

POST requests support idempotency via Idempotency-Key header. A retry with the same key returns the original response without re-executing.

POST /api/v1/contacts/
Idempotency-Key: create_contact_bob_20260423
...

Idempotency keys are retained for 24 hours. Keys must be unique per operation.

Webhooks on Contact Events

Subscribe to contact events via the Webhooks API:

  • contact.created
  • contact.updated
  • contact.deleted
  • contact.enriched
  • contact.scored

Each event carries the full contact object in the data field.

Common Integration Patterns

Pattern 1: Nightly sync from internal CRM

Scheduled job queries the internal CRM for changed records, transforms to NimbusOS contact format, and bulk-creates via POST /api/v1/contacts/bulk/ with dedup_behavior=update.

Pattern 2: Real-time webhook ingestion

A webhook fires when a contact changes in your system. Handler calls POST /api/v1/contacts/ with Idempotency-Key set from the source event ID to handle retries cleanly.

Pattern 3: Score-based enrollment

Scheduled job queries for contacts where lead_score >= 70 and has_tag=['hot'], then enrolls them in a specific sequence via the enrollment endpoint.

Troubleshooting

"Bulk create returns 200 but only half the contacts were created"

Dedup behavior and suppression. Check the response body for a per-row status breakdown. Contacts that matched existing records were updated, not created; suppressed contacts were rejected.

"Contact appears in API but missing from UI list"

Filter mismatch. The UI has default filters (e.g., hide deleted). API does not apply those unless specified. Check is_deleted on the contact.

"Update returns 403 Forbidden"

Scope missing (contacts:write), or workspace mismatch. Check the API key's scopes.

"Custom field value not persisted"

The custom field does not exist in the workspace. Create it first via POST /api/v1/custom-fields/, then retry the contact update.

Frequently Asked Questions

Is there a search index that handles complex queries?

Yes. The search endpoint uses a Postgres-backed indexing layer. Complex filters run efficiently up to millions of contacts.

Can I query across workspaces with one API call?

No. Workspace isolation is enforced. Multi-workspace agencies make one call per workspace.

How fast are bulk operations?

A 1,000 contact bulk create completes in 2 to 10 seconds typically. Enrichment if triggered runs asynchronously after the bulk response returns.

Is there a GraphQL endpoint?

No. REST only. GraphQL is a roadmap item for enterprise.

Useful next pages after this one: Campaigns API for campaign operations, Authentication for the token model, and Rate Limits for the throughput envelope.

Related articles

Still stuck?

Our team answers every support ticket. If the answer is not in the docs, open a ticket and we will write the missing page.