Smarter Contact API (1)

Download OpenAPI specification:

Read contacts and conversations, and reply to contacts who wrote first, from your own code or an AI agent.

Base URL: https://api.smartercontact.com/rest. Every resource is versioned on its own path (/contacts/v1).

Errors from the API come back as JSON: { "code": "...", "message": "..." }. A 429 also carries retryAfterSeconds in the body and a Retry-After header. The OAuth endpoints answer in the OAuth shape instead: { "error": "...", "error_description": "..." }.

Limits (defaults):

  • Sends: 300 per account per UTC hour. Every send attempt counts, including one refused with REPLY_REQUIRED, REPLY_TOO_SOON or BAD_REQUEST. A RATE_LIMITED send waits until the next UTC hour.
  • Reads: 3 requests per second per account per scope. REST and MCP share the bucket: GET /contacts/v1/{id} and the read_contact tool count against the same one.
  • Per IP: 120 failed requests per minute (a bad or missing token, for example). Successful requests are not counted.
  • Replies: only to a contact who wrote inside the last 7 days, and at least 60 seconds after their last inbound message or your last message to them.
  • Connected clients: 50 per account (clients the user signed in with, such as an MCP client). Your own credentials from the API tab do not count. Past the limit, the OAuth sign-in fails with access_denied.

Every authenticated response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix seconds).

Before using the API, the account owner accepts the Developer Terms on the Developer Platform page. Until then every call returns 403 TERMS_NOT_ACCEPTED.

Contacts

The people you text.

Read a contact

Authorizations:
oauth2
path Parameters
id
required
string

The contact id.

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "status": "string",
  • "createdAt": "2019-08-24T14:15:22Z",
  • "phone": "string",
  • "email": "string",
  • "firstName": "string",
  • "lastName": "string",
  • "favorite": true,
  • "dealClosed": true,
  • "unsubscribed": true,
  • "isOptIn": true,
  • "fields": [
    ]
}

Messages

Conversation history and replies.

Read a conversation

Messages with one contact, newest first. Pass nextId and nextCreatedAt from the previous page to get the next one.

Authorizations:
oauth2
path Parameters
id
required
string

The contact id.

query Parameters
limit
integer [ 1 .. 100 ]
Default: 50
nextId
string

Cursor from the previous page. Requires nextCreatedAt.

nextCreatedAt
string <date-time>

Cursor from the previous page, an ISO date.

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "hasNext": true,
  • "nextId": "string",
  • "nextCreatedAt": "2019-08-24T14:15:22Z"
}

Reply to a contact

Sends an SMS reply. It cannot start a conversation: the contact must have written inside the reply window (7 days by default), and the reply must come at least 60 seconds after their last inbound message and after the account's last message to them (from any source: campaign, inbox, API or MCP). Nothing is queued: on 429 REPLY_TOO_SOON wait retryAfterSeconds and retry.

Authorizations:
oauth2
path Parameters
id
required
string

The contact id.

Request Body schema: application/json
required
content
required
string non-empty

The message text, not empty or only spaces.

Responses

Request samples

Content type
application/json
{
  • "content": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "direction": "string",
  • "type": "string",
  • "content": "string",
  • "status": "string",
  • "createdAt": "2019-08-24T14:15:22Z"
}

Webhooks

Add endpoints on the Webhooks tab of the Developer Platform page (up to 16), each subscribed to the events it needs.

Delivery: a POST with a JSON body. Any non-2xx answer, or no answer, is retried: up to 3 attempts in total, with backoff from 1 to 60 seconds. Answer 2xx fast and process later.

Every body also carries email (the account's email) and, for a test sent from the Webhooks tab, "test": true. Tell the events apart by the body: an incoming SMS has "direction": "incoming" and no event field, a status update has "event": "message.status_updated".

Headers: the custom headers you set on the endpoint are sent with every delivery.

Test deliveries from the Webhooks tab are sent once, with no retry, a 10 second timeout and no redirects followed.

Signature: when the account has a signing secret (Webhooks tab, starts with whsec_), each delivery has the header X-SC-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with the secret>. Verify against the raw body bytes, not re-serialized JSON. t is set when the delivery is first queued and stays the same on every retry, so allow for the retry backoff when you check its age.

const crypto = require('crypto');
function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(',').map(p => p.split('=')));
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  return fresh && typeof v1 === 'string' && v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    fresh = abs(time.time() - int(parts["t"])) < 300
    return fresh and hmac.compare_digest(expected, parts["v1"])

A contact texted you Webhook

Flat body with no event field, kept as it is for existing integrations.

Request Body schema: application/json
direction
string
Value: "incoming"
senderId
string

The contact id.

senderPhoneNumber
string
recipientPhoneNumber
string
content
string
createdAt
string <date-time>
email
string
test
boolean

Responses

Request samples

Content type
application/json
{
  • "direction": "incoming",
  • "senderId": "3f2a9c1e-5b7d-4e8a-9c0f-1a2b3c4d5e6f",
  • "senderPhoneNumber": "+15551234567",
  • "recipientPhoneNumber": "+15557654321",
  • "content": "Yes, still interested",
  • "createdAt": "2026-10-01T15:04:05.000Z",
  • "email": "owner@example.com"
}

A message you sent was delivered or not Webhook

Only final states are sent: delivered or undelivered (a failed send is also reported as undelivered). reason explains an undelivered when known, otherwise null.

A test from the Webhooks tab has an id starting with evt_, random message_id and contact_id, source: null, and no from_number or to_number.

Request Body schema: application/json
id
string

Event id, use it to dedupe retries.

event
string
Value: "message.status_updated"
created_at
string <date-time>
email
string
test
boolean
object

Responses

Request samples

Content type
application/json
{
  • "id": "9b1d6f0e-2c3a-4f5b-8d7e-6a5b4c3d2e1f",
  • "event": "message.status_updated",
  • "created_at": "2026-10-01T15:05:10.000Z",
  • "email": "owner@example.com",
  • "data": {
    }
}