Docs · Public API
Read & write your orgfrom anywhere.
A Bearer-authenticated REST API at api.turtini.com. CRM contacts, accounts, opportunities, articles, events. Paginated reads, idempotent writes, OpenAPI 3.1-described surface, scoped per API key.
Authentication
Bearer token, scoped per key.
Every request carries an Authorization: Bearer turtini_… header. The token determines which org you're acting on and which scopes are allowed. Mint keys with the smallest scope set that does what you need.
# Read scopes — safe to start exploring contacts:read accounts:read opportunities:read articles:read events:read # Write scopes — only when you want the agent to actually ship contacts:write articles:write events:write
Scopes are checked on every request. A read key calling a POST endpoint returns 403 with a clear error.
Endpoints
Nine endpoints, consistently shaped.
Read endpoints share { data, nextPageToken }; writes return the created record.
| Method | Path | Scope | What it does |
|---|---|---|---|
| GET | /v1/auth/verify | any | Returns the org id, scope list, and key fingerprint for the bearer token. Use to confirm wiring before calling anything else. |
| GET | /v1/contacts | contacts:read | List people records in the org. Paginated; supports filter by email or accountId. |
| POST | /v1/contacts | contacts:write | Create a contact. Idempotency-Key honored; duplicate keys within 24h return the original record. |
| GET | /v1/accounts | accounts:read | List CRM accounts (companies / partners / vendors). |
| GET | /v1/opportunities | opportunities:read | List sales opportunities with stage, amount, close date, accountId. |
| GET | /v1/articles | articles:read | List articles. Filter by status (draft / published) and visibility. |
| POST | /v1/articles | articles:write | Create an article. Status is forced to "draft" — publish via the in-app Articles page. |
| GET | /v1/events | events:read | List org events with start time, capacity, ticket types. |
| POST | /v1/events | events:write | Create an event with title, dates, capacity, ticket-type definitions. |
The full per-endpoint shape (request body, response body, error codes) lives in the OpenAPI spec. Generate a typed client in 50+ languages with openapi-generator.
Quick start
Pick your client.
TypeScript SDK
npm install @turtini/sdkimport { Turtini } from '@turtini/sdk'
const t = new Turtini({ apiKey: process.env.TURTINI_API_KEY! })
// Read — auto-paginated iterator
for await (const c of t.contacts.iter()) console.log(c.email)
// Write — idempotency key auto-generated
await t.contacts.create({
firstName: 'Ada',
lastName: 'Lovelace',
email: '[email protected]',
})curl
No install — Bearer auth# Verify the key
curl https://api.turtini.com/v1/auth/verify \
-H "Authorization: Bearer turtini_<your_key>"
# List contacts (first 100)
curl https://api.turtini.com/v1/contacts \
-H "Authorization: Bearer turtini_<your_key>"
# Page 2
curl "https://api.turtini.com/v1/contacts?pageToken=<token>" \
-H "Authorization: Bearer turtini_<your_key>"
# Create a contact (idempotent)
curl https://api.turtini.com/v1/contacts \
-H "Authorization: Bearer turtini_<your_key>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"firstName":"Ada","lastName":"Lovelace","email":"[email protected]"}'Python (any HTTP client)
No SDK yet — use httpx or requestsimport os, uuid, httpx
API = "https://api.turtini.com/v1"
KEY = os.environ["TURTINI_API_KEY"]
HEAD = {"Authorization": f"Bearer {KEY}"}
# Read with pagination
def iter_contacts():
token = None
while True:
params = {"pageToken": token} if token else {}
res = httpx.get(f"{API}/contacts", headers=HEAD, params=params)
res.raise_for_status()
body = res.json()
for c in body["data"]:
yield c
token = body.get("nextPageToken")
if not token:
break
# Write with idempotency
res = httpx.post(
f"{API}/contacts",
headers={**HEAD, "Idempotency-Key": str(uuid.uuid4())},
json={"firstName": "Ada", "lastName": "Lovelace", "email": "[email protected]"},
)OpenAPI spec
Public — no auth# Download the OpenAPI 3.1 spec curl https://api.turtini.com/v1/openapi.json -o turtini-openapi.json # Generate a client in 50+ languages with openapi-generator npx @openapitools/openapi-generator-cli generate \ -i turtini-openapi.json \ -g typescript-fetch \ -o ./client
Pagination
Cursor-based, opaque tokens.
GET /v1/contacts?limit=100
↓
{
"data": [...],
"nextPageToken": "eyJj…"
}
GET /v1/contacts?pageToken=eyJj…
↓
{
"data": [...],
"nextPageToken": null
}Default 100, max 500. Tokens are opaque — don't parse them. Iterate until nextPageToken is null.
Idempotency
Optional but recommended.
POST /v1/contacts
Idempotency-Key: 9f1c… (UUID)
{ "email": "[email protected]", … }
↓
{ "id": "ctc_…", "email": "ada…", … }
# Same key + same body within 24h
↓ 200, same record. No duplicate.
# Same key + different body
↓ 409 Conflict.The SDK auto-generates a UUID per write. Override when you have a natural idempotency key (order id, request id, etc.).
FAQ
The honest answers.
How do I get an API key?
In the Turtini app, open Settings → Partner Dev → API Keys and click Create key. Pick the scopes you want this key to have — start with the read scopes for safe exploration. The plaintext key (turtini_…) is shown once, on creation; copy it before closing the dialog.
What's the rate limit?
The default cap is generous for SMB workloads. The SDK retries 429s with exponential backoff automatically; if you're writing your own client, watch for the standard Retry-After header and back off accordingly. Sustained high volume is a sales conversation, not a paywall — get in touch.
Can I use the SDK from a browser?
Yes for read scopes. The SDK warns once if you use a write scope from a browser context (don't ship a write key in client JS — it's extractable). For browser apps with write needs, use a server-side proxy or our turtini-dev CLI which injects auth server-side.
Are writes idempotent?
Optionally — pass an Idempotency-Key header on any POST. Same key + same body within 24 hours returns the original record without re-executing. Same key + different body is a 409. The @turtini/sdk auto-generates a UUID per write; you can override.
How does pagination work?
Read endpoints return { data: [...], nextPageToken: string | null }. Pass ?pageToken=… on the next request. Default page size is 100, max 500. The token is opaque (don't parse it). The SDK exposes async iterators that handle this transparently: for await (const c of t.contacts.iter()) ...
Where does my data live?
In your org's Firestore documents (Google Cloud, us-central1). The Public API is a thin Cloud Run service that authorizes, scopes, and returns those documents. There is no separate API database — what you see in the Turtini app and what the API returns are the same records.
How do I revoke a key?
Same place you minted it: Settings → Partner Dev → API Keys → Delete. Revocation propagates within seconds. Any in-flight request with the killed key returns 401 immediately.
Do I need a webhook?
For most integration patterns, no — read on demand from the API. Webhooks for create / update events are on the roadmap; if you need them today, get in touch and we'll prioritize based on the use case.
Mint a key.Build something.
Base URL https://api.turtini.com/v1. SDK on npm. OpenAPI spec for any language.