emberry

Developer API

Pull your feedback and cases into other tools, push events back, and get a live ping when something happens. Available on Premium and Enterprise.

Your first request in 60 seconds

Create a key in Setup → Developer (live or test), then:

curl https://www.emberry.com.au/api/v1/submissions \ -H "Authorization: Bearer sk_live_your_key_here"

You get JSON back, newest feedback first, with a cursor for the next page:

{ "data": [ { "id": "sub_abc", "site_id": "site_123", "sentiment": "positive", "nps_score": 9, "submitted_at": "2026-06-01T03:21:00.000Z", "answers": [ { "question_id": "q1", "widget": "stars", "value": 5 } ] } ], "next_cursor": "v1:eyJ…" }

Every response carries an x-request-id header (and a request_id on errors) — quote it if you contact support.

Authentication & scopes

Send your secret key as a Bearer token. A key looks like sk_live_… (or sk_test_… for testing); treat it like a password and never put it in front-end code. Each key only does what you tick when you create it: read scopes gate GET, write scopes gate POST and PATCH. Ask for the least you need.

ScopeLets the key…
submissions:readRead feedback (personal details scrubbed; no contact info)
submissions:read_piiAlso see contact details + raw free text on feedback
cases:readRead cases
cases:writeUpdate a case (status, priority, assignee, summary)
sites:readRead locations and surface areas
sites:writeUpdate a location
links:readRead feedback links
links:writeUpdate a feedback link
respondents:writeAttach a note to a respondent who already has feedback history
analytics:readRead headline numbers (NPS, overview)

Endpoints

All paths are under https://www.emberry.com.au/api/v1.

The API is read + update: discover records with GET and change existing ones with PATCH. Creating sites, links, and cases happens in the app (those need billing, plan limits, and context an external caller doesn’t have), so their POST routes aren’t available here. The one create is a respondent note.

Method & pathScopeNotes
GET /submissionssubmissions:readList feedback, newest first
GET /submissions/{id}submissions:readOne submission
GET /casescases:readList cases
POST /casescases:writeNot available via the API yet — open cases in the app; the API can read + update them
GET /cases/{id}cases:readOne case
PATCH /cases/{id}cases:writeUpdate status / priority / assigned_to / summary. Resolving fires case.resolved
GET /sitessites:readList locations
POST /sitessites:writeNot available via the API yet — adding a location is a billed change, so use Setup for now
GET /sites/{id}sites:readOne location
PATCH /sites/{id}sites:writeUpdate a location
GET /linkslinks:readList feedback links
POST /linkslinks:writeNot available via the API yet — create links in Setup for now
GET /links/{id}links:readOne link
PATCH /links/{id}links:writeUpdate label / status
GET /surface-areassites:readList the topics feedback is grouped under
POST /respondentsrespondents:writeAttach a note to a respondent. Body: email (required), note (required). Appears on that person’s record; the email must already have identified feedback
GET /analytics/npsanalytics:readNPS + promoter/passive/detractor counts
GET /analytics/overviewanalytics:readTotal responses, average rating, open cases

Analytics responses include a sampled flag — true only for very large accounts, where the numbers cover a recent sample rather than your whole history.

Pagination

List endpoints take ?limit= (max 100, default 50) and return a next_cursor. When it isn’t null, pass it back as ?cursor= for the next page until it is. A cursor is opaque — don’t parse it.

cursor = null repeat: res = GET /submissions?limit=100 (+ &cursor=cursor if set) process res.data cursor = res.next_cursor until cursor is null

Rate limits

Each key has a per-minute budget (Premium 240, Enterprise 600; subject to change). Over the limit you get 429 rate_limited with a Retry-After header in seconds — back off for that long and retry.

Errors

Every error has the same shape — { "error": "…", "detail?": "…", "request_id": "…" } — with a matching HTTP status.

StatuserrorMeaning
400bad_requestMalformed request (bad JSON, bad cursor)
401unauthorizedMissing / invalid / revoked / expired key
403plan_requiredThe developer API isn’t in your plan
403feature_disabledThe API isn’t switched on for your account yet
403insufficient_scopeThe key lacks the required scope
404not_foundNo such record (or it belongs to another account)
422unprocessableValidation failed (see detail)
429rate_limitedSlow down (see Retry-After)
5xxserver_errorOur side. Retry with backoff; quote request_id

Idempotency

Write endpoints (POST and PATCH) take an idempotency key. Send the same key on a retry and you get the original result back instead of the write happening twice. Pass it as the ?idempotency_key= query parameter (recommended) or the Idempotency-Key header:

curl -X POST "https://www.emberry.com.au/api/v1/cases?idempotency_key=7f3c1e90-9b2a-4f1e-bb0a-1d2c3e4f5a6b" \ -H "Authorization: Bearer sk_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "site_id": "site_123", "summary": "Callback requested" }'

Pick a unique key per logical request (a UUID works well). We remember the first successful response for 24 hours, scoped to your account, and replay it for any retry with the same key. Read endpoints ignore it.

Webhooks

Add an HTTPS endpoint in Setup → Developer and we will POST a signed message for each event you choose (submission.created, case.opened, case.resolved, nps.detractor). Verify it by recomputing the HMAC over "<timestamp>.<raw body>" with your signing secret:

const crypto = require("node:crypto"); function verify(rawBody, header, secret) { const parts = Object.fromEntries(header.split(",").map((p) => p.split("="))); const t = Number(parts.t); if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // 5 min const expected = crypto .createHmac("sha256", secret) .update(`${t}.${rawBody}`) .digest("hex"); const a = Buffer.from(expected, "hex"); const b = Buffer.from(parts.v1 || "", "hex"); return a.length === b.length && crypto.timingSafeEqual(a, b); }

Verify against the raw request body (not a re-serialized JSON object). Failed deliveries retry with backoff; after repeated failures we turn the endpoint off and email your account. Every delivery carries a stable Webhook-Id so you can dedup.

Reference

We only add to v1 — new fields and endpoints, never a breaking change. Ignore JSON fields you don’t recognise. Powered by emberry.