API Documentation
Build powerful integrations with our RESTful API. Everything you need to get started.
Quick Start
Base URL
https://your-account.graycrm.io/api/v1
Authentication
Include your API key in the Authorization header as a Bearer token. All requests must include Content-Type: application/json.
curl https://your-account.graycrm.io/api/v1/contacts \
-H "Authorization: Bearer your_api_key_here" \
-H "Content-Type: application/json"
Authentication & Scopes
API keys have scoped permissions. When creating an API key, select only the scopes your integration needs.
Available Scopes
contacts:readcontacts:writeproperties:readproperties:writetags:readtags:writenotes:readnotes:writeactivities:readactivities:writeflags:readflags:writecustom_attributes:readcustom_attributes:writeaudit_events:readbatch:writesearch:readRate Limiting
API requests are rate-limited based on your subscription plan. Rate limits reset daily at midnight UTC.
Sandbox
1,000
requests/day
Starter
100,000
requests/day
Professional
1M
requests/day
Rate Limit Headers
Every API response includes rate limit information in the headers.
X-RateLimit-Limit: 100000
X-RateLimit-Remaining: 99876
X-RateLimit-Reset: 1738281600
Pagination
List endpoints return paginated results. Use the page and per_page parameters to navigate. Default is 25 items per page.
GET /api/v1/contacts?page=2&per_page=50
{
"data": [...],
"pagination": {
"current_page": 2,
"total_pages": 10,
"total_count": 487,
"per_page": 50
}
}
Idempotency
Send an Idempotency-Key header on POST requests to safely retry failed requests without creating duplicates. The key can be any unique string (UUID recommended). Responses are cached for 24 hours.
curl -X POST https://your-account.graycrm.io/api/v1/contacts \
-H "Authorization: Bearer your_api_key" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-H "Content-Type: application/json" \
-d '{"contact": {"first_name": "Jane"}}'
API Status
Check API health status. No authentication required.
GET /api/v1/status
{
"status": "ok",
"version": "v1",
"timestamp": "2026-02-10T12:00:00Z"
}
Contacts
Manage contacts and people. Requires contacts:read or contacts:write scope.
List Contacts
Supports pagination, Ransack filtering, and tag/flag query params.
GET /api/v1/contacts
| Param | Description |
|---|---|
tag |
Filter by tag name(s). Comma-separated for multiple: ?tag=vip,hot-lead |
tag_mode |
and (default) or or. Controls multi-tag matching. |
flag[key] |
Filter by flag key: ?flag[key]=needs_review |
flag[value] |
Optional. Filter by flag value: ?flag[key]=status&flag[value]=pending |
attr[key] |
Filter by custom attribute key: ?attr[key]=vip_level |
attr[value] |
Optional. Filter by attribute value: ?attr[key]=vip_level&attr[value]=gold |
app_tag |
Filter to records enrolled in your app: ?app_tag=app:my_app. See App-Tag Segregation. |
scope |
Set to account to bypass app_tag filter for discovery: ?app_tag=app:my_app&scope=account |
Get Contact
Returns contact with emails, phones, tags, flags, and custom attributes.
GET /api/v1/contacts/:id
Create Contact
Create a new contact. Supports nested emails and phones via contact_emails_attributes and contact_phones_attributes.
POST /api/v1/contacts
{
"contact": {
"first_name": "Jane",
"last_name": "Doe",
"company": "Acme Corp",
"source_detail": "website signup",
"contact_emails_attributes": [
{
"email": "jane@example.com",
"label": "work",
"primary": true
}
],
"contact_phones_attributes": [
{
"phone": "+1-555-0100",
"label": "mobile",
"primary": true
}
]
}
}
Update Contact
PATCH /api/v1/contacts/:id
Delete Contact
Soft delete. Returns 204 No Content.
DELETE /api/v1/contacts/:id
Contact Emails
Manage email addresses for contacts. Requires contacts:write scope.
List Contact Emails
GET /api/v1/contacts/:contact_id/contact_emails
Create Contact Email
POST /api/v1/contacts/:contact_id/contact_emails
{
"contact_email": {
"email": "jane.work@example.com",
"label": "work",
"primary": false
}
}
Update Contact Email
PATCH /api/v1/contacts/:contact_id/contact_emails/:id
Delete Contact Email
DELETE /api/v1/contacts/:contact_id/contact_emails/:id
Contact Phones
Manage phone numbers for contacts. Requires contacts:write scope.
List Contact Phones
GET /api/v1/contacts/:contact_id/contact_phones
Create Contact Phone
POST /api/v1/contacts/:contact_id/contact_phones
{
"contact_phone": {
"phone": "+1-555-0200",
"label": "home",
"primary": false
}
}
Update Contact Phone
PATCH /api/v1/contacts/:contact_id/contact_phones/:id
Delete Contact Phone
DELETE /api/v1/contacts/:contact_id/contact_phones/:id
Properties
Manage physical addresses and locations linked to contacts. Requires properties:read or properties:write scope.
List Properties
Supports pagination, Ransack filtering, and tag/flag query params.
GET /api/v1/properties
| Param | Description |
|---|---|
tag |
Filter by tag name(s). Comma-separated for multiple: ?tag=vip,hot-lead |
tag_mode |
and (default) or or. Controls multi-tag matching. |
flag[key] |
Filter by flag key: ?flag[key]=needs_review |
flag[value] |
Optional. Filter by flag value: ?flag[key]=status&flag[value]=pending |
attr[key] |
Filter by custom attribute key: ?attr[key]=lot_size |
attr[value] |
Optional. Filter by attribute value: ?attr[key]=lot_size&attr[value]=large |
app_tag |
Filter to records enrolled in your app: ?app_tag=app:my_app. See App-Tag Segregation. |
scope |
Set to account to bypass app_tag filter for discovery: ?app_tag=app:my_app&scope=account |
Get Property
Returns property with tags, flags, and custom attributes.
GET /api/v1/properties/:id
Create Property
POST /api/v1/properties
{
"property": {
"street": "123 Main St",
"city": "San Francisco",
"state": "CA",
"zip": "94102",
"country": "US",
"latitude": 37.7749,
"longitude": -122.4194,
"place_id": "ChIJIQBpAG2ahYAR_6128GcTUEo",
"source_detail": "MLS import"
}
}
Update Property
PATCH /api/v1/properties/:id
Delete Property
Soft delete. Returns 204 No Content.
DELETE /api/v1/properties/:id
Contact-Property Links
Link contacts to properties with optional role. Requires both contacts:write and properties:write scopes.
List Links for Contact
GET /api/v1/contacts/:contact_id/contact_properties
Create Link
Link a contact to a property. Also available at /api/v1/properties/:property_id/contact_properties with contact_property[contact_id].
POST /api/v1/contacts/:contact_id/contact_properties
{
"contact_property": {
"property_id": "550e8400-e29b-41d4-a716-446655440000",
"role": "owner" // owner | tenant | manager | other
}
}
Remove Link
Returns 204 No Content.
DELETE /api/v1/contacts/:contact_id/contact_properties/:id
Taggings
Assign and remove tags from contacts or properties. Requires tags:write scope. Note: The nested route is /taggings, not /tags.
List Taggings
Returns all tags assigned to a contact or property. Also available on /api/v1/properties/:property_id/taggings.
GET /api/v1/contacts/:contact_id/taggings
Add Tag
Add a tag by name (finds or creates) or by ID. Also available on properties.
POST /api/v1/contacts/:contact_id/taggings
{
"tagging": {
"tag_name": "vip"
}
}
// OR
{
"tagging": {
"tag_id": "550e8400-e29b-41d4-a716-446655440000"
}
}
Remove Tag
Returns 204 No Content.
DELETE /api/v1/contacts/:contact_id/taggings/:id
Custom Attributes
Store arbitrary key-value data on contacts or properties. Requires custom_attributes:read or custom_attributes:write scope.
List Custom Attributes
Also available on /api/v1/properties/:property_id/custom_attributes.
GET /api/v1/contacts/:contact_id/custom_attributes
Create Custom Attribute
POST /api/v1/contacts/:contact_id/custom_attributes
{
"custom_attribute": {
"key": "subscription_tier",
"value": "premium"
}
}
Update Custom Attribute
PATCH /api/v1/contacts/:contact_id/custom_attributes/:id
Delete Custom Attribute
Returns 204 No Content.
DELETE /api/v1/contacts/:contact_id/custom_attributes/:id
Flags
Boolean flags for workflow management. Requires flags:read or flags:write scope.
List Flags
Also available on /api/v1/properties/:property_id/flags.
GET /api/v1/contacts/:contact_id/flags
Create Flag
POST /api/v1/contacts/:contact_id/flags
{
"flag": {
"key": "high_value",
"value": true
}
}
Update Flag
PATCH /api/v1/contacts/:contact_id/flags/:id
Claim Flag
Claim a pending flag (workflow action).
POST /api/v1/contacts/:contact_id/flags/:id/claim
Delete Flag
Returns 204 No Content.
DELETE /api/v1/contacts/:contact_id/flags/:id
Notes
Add notes to contacts or properties. Requires notes:read or notes:write scope.
List Notes
Ordered by most recent. Also available on /api/v1/properties/:property_id/notes.
GET /api/v1/contacts/:contact_id/notes
Create Note
POST /api/v1/contacts/:contact_id/notes
{
"note": {
"body": "Called customer to discuss pricing."
}
}
Update Note
PATCH /api/v1/contacts/:contact_id/notes/:id
Delete Note
Returns 204 No Content.
DELETE /api/v1/contacts/:contact_id/notes/:id
Activities
Track interactions and events. Requires activities:read or activities:write scope.
List Activities
Ordered by most recent. Also available on /api/v1/properties/:property_id/activities.
GET /api/v1/contacts/:contact_id/activities
Create Activity
POST /api/v1/contacts/:contact_id/activities
{
"activity": {
"activity_type": "phone_call",
"subject": "Intro call",
"body": "Discussed pricing and timeline.",
"occurred_at": "2026-02-10T14:30:00Z"
}
}
Update Activity
PATCH /api/v1/contacts/:contact_id/activities/:id
Delete Activity
Returns 204 No Content.
DELETE /api/v1/contacts/:contact_id/activities/:id
Audit Events
Complete audit trail of all changes. Requires audit_events:read scope.
List All Audit Events
Paginated and filterable via Ransack.
GET /api/v1/audit_events
Get Audit Event
GET /api/v1/audit_events/:id
List Audit Events for Contact
GET /api/v1/contacts/:contact_id/audit_events
List Audit Events for Property
GET /api/v1/properties/:property_id/audit_events
Search
Full-text search across contacts and properties. Requires search:read scope.
Search
Searches both contacts and properties. Optional per_resource param controls results per type.
GET /api/v1/search?q=john+smith&per_resource=10
{
"data": {
"contacts": [...],
"properties": [...]
},
"meta": {
"query": "john smith",
"total_count": 15
}
}
Batch Operations
Process up to 100 operations in a single request. Requires batch:write scope plus appropriate resource scopes.
Batch Request
Valid methods: POST, PATCH, DELETE. Valid resources: contacts, properties. Max 100 operations per request.
POST /api/v1/batch
{
"operations": [
{
"method": "POST",
"resource": "contacts",
"body": {
"first_name": "Jane"
}
},
{
"method": "PATCH",
"resource": "contacts",
"id": "550e8400-e29b-41d4-a716-446655440000",
"body": {
"last_name": "Doe"
}
},
{
"method": "DELETE",
"resource": "properties",
"id": "660e8400-e29b-41d4-a716-446655440002"
}
]
}
Webhooks
Subscribe to real-time events. Manage webhooks via the web UI.
Available Events
contact.createdcontact.updatedcontact.deletedproperty.createdproperty.updatedproperty.deletednote.creatednote.updatednote.deletedtagging.createdtagging.deletedflag.createdflag.updatedflag.deletedcustom_attribute.createdcustom_attribute.updatedcustom_attribute.deletedcontact_property.createdcontact_property.deletedExample Webhook Payload
{
"event": "contact.updated",
"timestamp": "2026-02-10T12:00:00Z",
"account_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"record": { ... },
"changes": {
"first_name": ["John", "Jane"]
}
}
}
App-Tag Segregation
Partition contacts and properties into per-application views using tags. Each integration can maintain its own slice of your CRM data without interfering with others.
How It Works
Create a tag using the app: naming convention (e.g., app:lawn_care). Tag contacts and properties to enroll them in your app. Then pass the app_tag query parameter on list endpoints to filter results to only enrolled records.
Workflow
Create your app tag
POST /api/v1/tags with name app:my_app
Discover records
GET /api/v1/contacts?app_tag=app:my_app&scope=account to search the full account
Enroll records
POST /api/v1/contacts/:id/taggings with tag_name app:my_app
Query enrolled records
GET /api/v1/contacts?app_tag=app:my_app returns only enrolled contacts
Unenroll records
DELETE /api/v1/contacts/:id/taggings/:tagging_id to remove the app tag
Query Parameters
| Param | Description |
|---|---|
app_tag |
Tag name for app-level filtering. Convention: app:your_app_name |
scope |
Set to account to bypass the app_tag filter and search all records (discovery mode) |
Examples
List only enrolled contacts:
GET /api/v1/contacts?app_tag=app:lawn_care
Discover all contacts in the account (bypass filter):
GET /api/v1/contacts?app_tag=app:lawn_care&scope=account
Enroll a contact:
POST /api/v1/contacts/:id/taggings
{
"tagging": {
"tag_name": "app:lawn_care"
}
}
Works with properties too:
GET /api/v1/properties?app_tag=app:lawn_care
Multiple Apps, One Account
Each integration uses its own app tag. A contact can be enrolled in multiple apps simultaneously. Tags are lightweight and do not duplicate data. All standard filters (Ransack, flags, attributes) work alongside app_tag.
Error Handling
All errors return a consistent JSON structure with HTTP status codes.
HTTP Status Codes
200
OK - Request succeeded
201
Created - Resource created successfully
204
No Content - Delete succeeded, no body returned
400
Bad Request - Invalid parameters
401
Unauthorized - Invalid or missing API key
403
Forbidden - Insufficient scope or account suspended
404
Not Found - Resource does not exist
422
Unprocessable Entity - Validation errors
429
Too Many Requests - Rate limit exceeded
500
Internal Server Error - Something went wrong on our end
Error Response Format
{
"error": {
"type": "validation_error",
"message": "First name is required",
"details": {
"first_name": ["can't be blank"]
}
}
}