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.

MethodPathScopeWhat it does
GET/v1/auth/verifyanyReturns the org id, scope list, and key fingerprint for the bearer token. Use to confirm wiring before calling anything else.
GET/v1/contactscontacts:readList people records in the org. Paginated; supports filter by email or accountId.
POST/v1/contactscontacts:writeCreate a contact. Idempotency-Key honored; duplicate keys within 24h return the original record.
GET/v1/accountsaccounts:readList CRM accounts (companies / partners / vendors).
GET/v1/opportunitiesopportunities:readList sales opportunities with stage, amount, close date, accountId.
GET/v1/articlesarticles:readList articles. Filter by status (draft / published) and visibility.
POST/v1/articlesarticles:writeCreate an article. Status is forced to "draft" — publish via the in-app Articles page.
GET/v1/eventsevents:readList org events with start time, capacity, ticket types.
POST/v1/eventsevents:writeCreate 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/sdk
import { 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 requests
import 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.

Turtini uses cookies to improve your experience, analyze site traffic, and personalize content. By clicking Accept, you consent to our use of cookies. Privacy Policy

Wally

Your Turtini assistant

Hi, I'm Wally!

Ask me anything about Turtini — features, pricing, how things work, and more.

or

Already have an account? Sign in

Wally can make mistakes — verify important info.