Developer platform

One request. The whole feedback loop.

Send a completed appointment once, then get back to building your product. Reply Kindly handles the follow-up from there.

REST APISigned webhooksTest mode

Public endpoints are versioned under /v1. Built for a smooth path from first request to production.

Quickstart
API_BASE_URL="https://replykindly.com"
API_KEY="nps_test_..."

curl "$API_BASE_URL/v1/follow-ups" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: appointment-booking_83921" \
  -d '{
    "externalAppointmentId": "booking_83921",
    "customer": {
      "externalId": "customer_281",
      "name": "Jane Smith",
      "email": "jane@example.com"
    },
    "completedAt": "2026-08-21T02:30:00Z"
  }'
✓
201 CreatedFollow-up scheduled
fup_7Bj3Kd92

Built for the real world

Everything around the request is already considered.

Your first integration

Create a test API key in the dashboard, then send one completed appointment. The loop has everything it needs to get moving.

  • timing
  • email delivery
  • reminders
  • response collection
  • NPS classification

Idempotent by default

Every write endpoint supports an Idempotency-Key header, so retries are safe.

Test and live modes

A test API key never touches live customer emails or live feedback.

Webhooks

Subscribe to feedback.created and feedback.comment_added and get notified as responses arrive.

Your data stays yours

Structured feedback records with external appointment and customer IDs. Read them through the API, receive them by signed webhook, or download a CSV from the dashboard.

Safe to retry

Authentication that stays out of your way.

Send your API key as a bearer token. Test keys use the test environment; live keys send real customer emails.

Include an Idempotency-Key on writes. Reusing it with the same request safely returns the original result; a different request returns a conflict.

Request header
Authorization: Bearer nps_live_k7P3_...
Test keys never contact real customers.

API reference

Read and write the feedback loop without guesswork.

Every endpoint below includes its access requirement, typed parameters, an immediately usable request, and the response shape you should expect.

POST /v1/follow-ups Create a follow-upTell Reply Kindly that an appointment is complete. We schedule the customer follow-up using the settings for that account.
AccessAPI key with followups:writeAvailable toDirect and platform keys

Parameters

HeaderAuthorization
stringRequired

Bearer API key, for example Bearer nps_test_….

HeaderContent-Type
application/jsonRequired

Required for the JSON request body.

HeaderIdempotency-Key
stringOptional

Strongly recommended. Reuse it only when retrying the identical request.

BodyexternalAppointmentId
stringRequired

Your stable identifier for the completed appointment.

Bodycustomer.email
stringRequired

The customer address used for the follow-up.

BodycompletedAt
ISO-8601 datetimeRequired

When the appointment finished.

BodyexternalAccountId
stringOptional

Required for a platform key; omit for a direct key.

Bodycustomer.externalId / customer.name
stringOptional

Your customer reference and a customer-facing name.

Bodymetadata
objectOptional

Extra appointment context. Top-level string, number, and boolean values can later be used as exact-match report filters.

Example request

curl "$API_BASE_URL/v1/follow-ups" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: appointment-booking_83921" \
  -d '{
    "externalAppointmentId": "booking_83921",
    "customer": { "externalId": "customer_281", "name": "Jane Smith", "email": "jane@example.com" },
    "completedAt": "2026-08-21T02:30:00Z",
    "metadata": { "locationId": "auckland-central", "service": "colour" }
  }'

201 Created

{
  "id": "fup_7Bj3Kd92",
  "externalAppointmentId": "booking_83921",
  "metadata": { "locationId": "auckland-central", "service": "colour" },
  "status": "scheduled",
  "livemode": false,
  "createdAt": "2026-08-21T02:30:01.000Z",
  "updatedAt": "2026-08-21T02:30:01.000Z"
}
  • A test key records the workflow but never emails the supplied customer.
  • The same idempotency key and identical request returns the original result; reusing a key with different content returns 409.
GET /v1/follow-ups/:id Read a follow-upRetrieve the current scheduled follow-up by the ID returned when it was created.
AccessAPI key with followups:readAvailable toDirect and platform keys

Parameters

Pathid
stringRequired

The follow-up ID, such as fup_7Bj3Kd92.

QueryexternalAccountId
stringOptional

Required for a platform key; omit for a direct key.

Example request

curl "$API_BASE_URL/v1/follow-ups/fup_7Bj3Kd92" \
  -H "Authorization: Bearer $API_KEY"

200 OK

{
  "id": "fup_7Bj3Kd92",
  "externalAppointmentId": "booking_83921",
  "metadata": null,
  "status": "scheduled",
  "livemode": true,
  "createdAt": "2026-08-21T02:30:01.000Z",
  "updatedAt": "2026-08-21T02:30:01.000Z"
}
  • Returns 404 when the follow-up does not exist in the account and environment selected by the API key.
GET /v1/feedback List feedbackRead customer responses with cursor pagination and filters that map to the data your integration already owns.
AccessAPI key with feedback:readAvailable toDirect and platform keys

Parameters

QuerystartDate / endDate
ISO-8601 date or datetimeOptional

Inclusive response window. A date covers its full UTC day. from and to remain supported aliases.

Querysegment
promoter | passive | detractorOptional

Return only one NPS segment.

QueryexternalCustomerId / externalAppointmentId
stringOptional

Exact-match one of your identifiers.

Querymetadata.key
stringOptional

Exact-match a top-level metadata value, for example metadata.locationId=auckland-central.

Querylimit
integerOptional

Responses per page. Defaults to 50; values above 100 are capped at 100.

Querycursor
stringOptional

Use pagination.nextCursor from the preceding response.

QueryexternalAccountId
stringOptional

Required for a platform key; omit for a direct key.

Example request

curl --get "$API_BASE_URL/v1/feedback" \
  -H "Authorization: Bearer $API_KEY" \
  --data-urlencode "startDate=2026-08-01" \
  --data-urlencode "endDate=2026-08-31" \
  --data-urlencode "segment=detractor" \
  --data-urlencode "metadata.locationId=auckland-central"

200 OK

{
  "data": [{
    "id": "fbk_H7ks82",
    "externalAppointmentId": "booking_83921",
    "externalCustomerId": "customer_281",
    "customerName": "Jane Smith",
    "score": 4,
    "segment": "detractor",
    "comment": "The appointment started late.",
    "respondedAt": "2026-08-21T04:01:00.000Z",
    "commentSubmittedAt": "2026-08-21T04:02:00.000Z",
    "livemode": true
  }],
  "pagination": { "hasMore": false, "nextCursor": null }
}
  • Combine filters to require every value to match. Results are newest first.
GET /v1/feedback/:id Read one feedback responseRetrieve the full structured record for one feedback response.
AccessAPI key with feedback:readAvailable toDirect and platform keys

Parameters

Pathid
stringRequired

The feedback ID, such as fbk_H7ks82.

QueryexternalAccountId
stringOptional

Required for a platform key; omit for a direct key.

Example request

curl "$API_BASE_URL/v1/feedback/fbk_H7ks82" \
  -H "Authorization: Bearer $API_KEY"

200 OK

{
  "id": "fbk_H7ks82",
  "externalAppointmentId": "booking_83921",
  "externalCustomerId": "customer_281",
  "customerName": "Jane Smith",
  "score": 4,
  "segment": "detractor",
  "comment": "The appointment started late.",
  "respondedAt": "2026-08-21T04:01:00.000Z",
  "commentSubmittedAt": "2026-08-21T04:02:00.000Z",
  "livemode": true
}
  • Returns 404 when the record does not belong to the API key’s account or environment.
GET /v1/nps Calculate NPSCalculate NPS from the feedback matching your response window and optional filters.
AccessAPI key with nps:readAvailable toDirect and platform keys

Parameters

QuerystartDate / endDate
ISO-8601 date or datetimeOptional

Inclusive reporting window. Defaults to the last 30 days. from and to remain supported aliases.

QueryincludeAverage
booleanOptional

Set true (or use the average alias) to include the 0–10 averageScore.

Querysegment, externalCustomerId, externalAppointmentId, metadata.key
stringOptional

Use the same exact-match filters as the feedback endpoint.

QueryexternalAccountId
stringOptional

Required for a platform key; omit for a direct key.

Example request

curl --get "$API_BASE_URL/v1/nps" \
  -H "Authorization: Bearer $API_KEY" \
  --data-urlencode "startDate=2026-08-01" \
  --data-urlencode "endDate=2026-08-31" \
  --data-urlencode "includeAverage=true"

200 OK

{
  "from": "2026-08-01T00:00:00.000Z",
  "to": "2026-08-31T23:59:59.999Z",
  "nps": 62,
  "responseCount": 184,
  "promoters": 132,
  "passives": 34,
  "detractors": 18,
  "averageScore": 8.7,
  "livemode": true
}
  • NPS is a whole number from -100 to 100. With no matching responses, nps is null and averageScore is null when requested.
POST /v1/accounts Create a downstream accountFor platforms only: create the customer account that owns its own feedback settings and branding.
AccessPlatform API keyAvailable toPlatform keys only

Parameters

HeaderContent-Type
application/jsonRequired

Required for the JSON request body.

BodyexternalAccountId
stringRequired

Your stable ID for the downstream business. Must be unique in your platform.

Bodyname
stringRequired

The client business name; 1–255 characters.

Bodysettings.followUp
objectOptional

Optional overrides for send delay, reminders, expiry, and detractor alerts.

Bodysettings.branding
objectOptional

Optional sender name and logo URL overrides.

Example request

curl "$API_BASE_URL/v1/accounts" \
  -H "Authorization: Bearer $PLATFORM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "externalAccountId": "salon_882",
    "name": "Central Salon",
    "settings": { "branding": { "fromName": "Central Salon" } }
  }'

201 Created

{
  "id": "acc_4q0Wn",
  "externalAccountId": "salon_882",
  "name": "Central Salon",
  "status": "active",
  "settings": { "branding": { "fromName": "Central Salon" } },
  "createdAt": "2026-08-21T02:30:01.000Z",
  "updatedAt": "2026-08-21T02:30:01.000Z"
}
  • Use this externalAccountId in platform follow-up, feedback, NPS, and webhook-endpoint requests.
  • A duplicate externalAccountId returns 409.

Errors

Clear, structured errors

Every error has a stable code, a human-readable message, and a request ID when an unexpected error needs support.

{
  "error": {
    "code": "BAD_REQUEST",
    "message": "customer.email is required",
    "requestId": "req_..."
  }
}

For platforms

One platform key. Clearly isolated customer accounts.

Platform keys keep each downstream business isolated with externalAccountId. Create the mapping once, then include the same value when creating appointments and reading their follow-ups, feedback, or NPS.

Create an account
POST /v1/accounts

{
  "externalAccountId": "salon_882",
  "name": "Central Salon",
  "settings": {
    "followUp": { "sendDelayMinutes": 120 },
    "branding": { "fromName": "Central Salon" }
  }
}
1

Set the account

POST /v1/follow-ups

{
  "externalAccountId": "salon_882",
  "externalAppointmentId": "booking_83921",
  "customer": { "email": "jane@example.com" },
  "completedAt": "2026-08-21T02:30:00Z"
}
2

Make it recognisable

Upload a client logo and optionally set its sender name.

POST /v1/accounts/salon_882/logo

Content-Type: multipart/form-data

logoFile=@central-salon-logo.png
fromName=Central Salon
3

Read account feedback

curl "$API_BASE_URL/v1/feedback?externalAccountId=salon_882" \
  -H "Authorization: Bearer nps_platform_test_..."

Events, when they happen

Let feedback come to you.

Verify Reply-Kindly-Signature against the exact request body. The header uses t=<unix seconds>,v1=<HMAC-SHA256 of "timestamp.body">. Use Reply-Kindly-Event-Id to deduplicate events. Failed deliveries retry automatically and can be replayed from the dashboard.

feedback.createdjust now
{
  "id": "evt_8Ks921",
  "type": "feedback.created",
  "data": {
    "feedback": {
      "id": "fbk_H7ks82",
      "score": 4,
      "segment": "detractor"
    }
  }
}

Delivery contract

Verify, deduplicate, then process.

The signature is calculated over the raw bytes, not a re-serialised JSON object. Keep the original body until the signature has passed.

  1. Verify firstRead the unmodified request body, then verify Reply-Kindly-Signature before parsing JSON.
  2. Deduplicate secondStore Reply-Kindly-Event-Id before processing. A replay may deliver the same event again.
  3. Recover deliberatelyReturn a 2xx once accepted. Failed deliveries retry automatically and remain replayable from the dashboard.
Node.js exampleExact raw-body verification
import { createHmac, timingSafeEqual } from "node:crypto";

const rawBody = await request.text();
const signature = request.headers.get("Reply-Kindly-Signature") ?? "";
const eventId = request.headers.get("Reply-Kindly-Event-Id");
const parts = Object.fromEntries(signature.split(",").map((part) => part.split("=")));

const expected = createHmac("sha256", process.env.REPLY_KINDLY_WEBHOOK_SECRET)
  .update(`${parts.t}.${rawBody}`)
  .digest();
const received = Buffer.from(parts.v1 ?? "", "hex");

if (!parts.t || received.length !== expected.length || !timingSafeEqual(received, expected)) {
  return new Response("Invalid signature", { status: 401 });
}

if (await alreadyProcessed(eventId)) return new Response(null, { status: 204 });
await markProcessed(eventId);
const event = JSON.parse(rawBody);