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.
| Scope | Lets the key… |
|---|---|
submissions:read | Read feedback (personal details scrubbed; no contact info) |
submissions:read_pii | Also see contact details + raw free text on feedback |
cases:read | Read cases |
cases:write | Update a case (status, priority, assignee, summary) |
sites:read | Read locations and surface areas |
sites:write | Update a location |
links:read | Read feedback links |
links:write | Update a feedback link |
respondents:write | Attach a note to a respondent who already has feedback history |
analytics:read | Read 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 & path | Scope | Notes |
|---|---|---|
GET /submissions | submissions:read | List feedback, newest first |
GET /submissions/{id} | submissions:read | One submission |
GET /cases | cases:read | List cases |
POST /cases | cases:write | Not available via the API yet — open cases in the app; the API can read + update them |
GET /cases/{id} | cases:read | One case |
PATCH /cases/{id} | cases:write | Update status / priority / assigned_to / summary. Resolving fires case.resolved |
GET /sites | sites:read | List locations |
POST /sites | sites:write | Not available via the API yet — adding a location is a billed change, so use Setup for now |
GET /sites/{id} | sites:read | One location |
PATCH /sites/{id} | sites:write | Update a location |
GET /links | links:read | List feedback links |
POST /links | links:write | Not available via the API yet — create links in Setup for now |
GET /links/{id} | links:read | One link |
PATCH /links/{id} | links:write | Update label / status |
GET /surface-areas | sites:read | List the topics feedback is grouped under |
POST /respondents | respondents:write | Attach 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/nps | analytics:read | NPS + promoter/passive/detractor counts |
GET /analytics/overview | analytics:read | Total 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 nullRate 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.
| Status | error | Meaning |
|---|---|---|
| 400 | bad_request | Malformed request (bad JSON, bad cursor) |
| 401 | unauthorized | Missing / invalid / revoked / expired key |
| 403 | plan_required | The developer API isn’t in your plan |
| 403 | feature_disabled | The API isn’t switched on for your account yet |
| 403 | insufficient_scope | The key lacks the required scope |
| 404 | not_found | No such record (or it belongs to another account) |
| 422 | unprocessable | Validation failed (see detail) |
| 429 | rate_limited | Slow down (see Retry-After) |
| 5xx | server_error | Our 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
- OpenAPI 3.1 specification — the machine-readable mirror of everything above, for code generation
- Manage keys and webhooks in Setup → Developer
We only add to v1 — new fields and endpoints, never a breaking change. Ignore JSON fields you don’t recognise. Powered by emberry.