Writing data through the public API

The public API supports writes for the modules where it's safe and useful — contacts (CRM intake), articles (content drafts), and events (calendar drafts). Writes use the same Bearer-token auth as reads but require a corresponding *:write scope on the API key.

Available endpoints:

POST /v1/contacts — create a CRM contact
Body: { firstName, lastName, email, phone?, accountId?, title?, linkedinUrl? }
Returns: { id, ...contact } at 201

POST /v1/articles — create a draft article
Body: { title, body, category?, status?, slug?, tags? }
Status is forced to 'draft' regardless of input — admins still need to approve and publish in-app
Author is recorded as "API: <key name>" so you can audit which integration created what
Returns: { id, ...article } at 201

POST /v1/events — create a draft event
Body: { title, description?, startsAt, endsAt, location?, capacity?, ticketTypes? }
Defaults: status='draft', visibility='private' — promote to public from the Events admin
Returns: { id, ...event } at 201

Idempotency-Key (required for writes):
Pass an Idempotency-Key header on every POST. Use a UUID v4 per logical operation. We cache the response for 24h keyed on (orgId, key) — if you retry the same key, you get the cached response back instead of creating a duplicate. This is what makes retries safe across network failures.

curl https://api.turtini.com/v1/contacts \
-X POST \
-H "Authorization: Bearer turtini_<your-key>" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-H "Content-Type: application/json" \
-d '{"firstName":"Ada","lastName":"Lovelace","email":"[email protected]"}'

Concurrent retries: if two requests with the same Idempotency-Key arrive at once, the second polls for up to 5 seconds for the first to finish, then returns its cached response.

Cross-org safety: writes that reference another doc (e.g. POST /v1/contacts with accountId) verify the referenced doc belongs to the same org as the key. Cross-org references return 404.

Field allowlists: each endpoint silently drops fields that aren't in its allowlist — extra fields don't cause a 400. This makes the API forward-compatible with future client versions.

Common errors:
• 401 — missing / invalid / revoked key
• 403 — key lacks the required scope (e.g. POST /v1/articles needs articles:write or *:write)
• 404 — referenced doc not found in your org
• 422 — required field missing or malformed
• 429 — rate-limited (500/min per key)

If you're writing client code, use @turtini/sdk — it handles Idempotency-Key generation, retries, and typed errors for you. See "@turtini/sdk — typed Public API client".