Anleitungen

Running an integration in production

Keep a Simple Feedback integration running unattended: keys that outlive people, a startup check, safe retries, errors that need a person, and traceable logs.

Die Entwickler-Dokumentation gibt es nur auf Englisch, wie die API selbst.

The other guides show what to call. This one is about running those calls unattended for months: which key to use, what to retry, what to hand to a person, and what to log. The shell variables SF_API and SF_KEY are set up as in the Quickstart.

One key per integration

Create a separate key for every integration and every environment, named so that anyone in the dashboard can tell what uses it, for example "Booking sync (production)". Then:

  • Give it only the scopes it calls. A sync that reads responses needs surveys:read and responses:read, nothing that writes. Scopes lists them.
  • Limit it to one venue when the integration belongs to one location, such as a single shop's till or one hotel.
  • Create it from an account that outlives individual people. A key stops working when the member who created it leaves the organization, see A key acts for the person who created it.
  • Keep the token in your secret store. It is shown once and can't be recovered.

Settings → API keys shows when each key was last used. A key nothing has used for months can be revoked.

Replace a key without downtime

A key with an expiry stops working on that day with 401 API_KEY_EXPIRED, and the API doesn't warn you before. Put the date in your calendar when you create the key, and replace it a few days ahead:

  1. Create a new key with the same scopes and venue.
  2. Deploy your integration with the new token.
  3. Check in Settings → API keys that the new key is in use, then revoke the old one.

Do the same when a token may have leaked, but revoke the old key right away: its next request answers 401 API_KEY_REVOKED.

Check the key on startup

GET /v1/me needs no scope and tells you what the key may do. Call it when your integration starts and stop with a clear message if a scope is missing, instead of failing on the first real call hours later:

typescript
const REQUIRED_SCOPES = ['surveys:read', 'responses:read'];

const res = await fetch(`${process.env.SF_API}/v1/me`, {
  headers: { Authorization: `Bearer ${process.env.SF_KEY}` },
});
if (!res.ok) throw new Error(`Simple Feedback key rejected: ${res.status} ${await res.text()}`);
const me = (await res.json()) as { scopes: string[]; venueId: string | null };
const missing = REQUIRED_SCOPES.filter((scope) => !me.scopes.includes(scope));
if (missing.length > 0) throw new Error(`Simple Feedback key lacks ${missing.join(', ')}`);

Retry only what can succeed

Answer Retry?
429 Yes, after the Retry-After seconds
5xx, a timeout or a dropped connection Yes, a few times with growing pauses; a POST with the same Idempotency-Key
Any other 4xx No. The same request fails the same way

A POST that timed out may still have gone through. Sending it again with the same Idempotency-Key returns the first answer instead of creating a second survey or QR code, see Retrying safely. PATCH and DELETE can be repeated as they are.

typescript
const API = process.env.SF_API;
const KEY = process.env.SF_KEY;

async function call(method: string, path: string, body?: unknown): Promise<Response> {
  const headers: Record<string, string> = { Authorization: `Bearer ${KEY}` };
  if (body !== undefined) headers['Content-Type'] = 'application/json';
  if (method === 'POST') headers['Idempotency-Key'] = crypto.randomUUID();

  for (let attempt = 1; ; attempt++) {
    let res: Response | undefined;
    try {
      res = await fetch(`${API}${path}`, {
        method,
        headers,
        body: body === undefined ? undefined : JSON.stringify(body),
        signal: AbortSignal.timeout(30_000),
      });
    } catch (error) {
      if (attempt === 5) throw error;
    }
    if (res !== undefined && res.status !== 429 && res.status < 500) return res;
    if (res?.status === 429) {
      await sleep(Number(res.headers.get('Retry-After')) * 1000);
      continue;
    }
    if (attempt === 5) return res as Response;
    await sleep(2 ** attempt * 1000);
  }
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

The Idempotency-Key is created once per call, before the first attempt, so every retry of that call carries the same one.

Errors that need a person

Some answers mean the integration can't continue until someone changes something. Retrying them only fills your logs, so stop the job and alert whoever runs it:

error.code What someone has to do
API_KEY_INVALID Check that the whole token was deployed
API_KEY_REVOKED, API_KEY_EXPIRED Create a new key and deploy it
INSUFFICIENT_SCOPE Give the key the scope named in details.requiredScope
API_ACCESS_NOT_IN_TIER Move the organization to a plan with API access
ORG_SUSPENDED Ask the organization's owner why it isn't active
TIER_LIMIT_REACHED Delete something or move to a larger plan

Branch on error.code, never on the message. Errors lists every code.

Log what support needs

Every response carries an X-Request-Id header. Log it with the status and error.code of every failed call; with that id, support finds your request in our logs.

You can also send your own X-Request-Id, up to 128 characters, for example the id of your sync run. The response returns it unchanged, so your logs and ours share one id:

bash
curl -i "$SF_API/v1/me" -H "Authorization: Bearer $SF_KEY" -H "X-Request-Id: booking-sync-2026-10-15T06:00"

Pace bulk work

A key may send 60 requests per minute to each route. A nightly job that reads many surveys stays under that by watching X-RateLimit-Remaining and slowing down before it reaches 0. Rate limits has the details.

Change live surveys carefully

Changes through the API reach respondents with the next page load; there is no review step. Try a new kind of change on a draft first, and keep POST /v1/surveys/{id}/undo in mind: it puts a survey back the way it was before the last change made through the API, one step per call, see Change a question.