earful

earful api

OpenAPI 3.1.0 · version 1.0.0 · https://earful.onibaku.co.uk

Everything the Earful app does: find a business on review sites, add its review pages, read reviews as they arrive, draft replies with AI, post them, and get webhooks for new reviews.

Authenticate with Authorization: Bearer ef_…. Every plan includes the API; plan limits apply as in the app. Errors are RFC 9457 application/problem+json. Every keyed answer carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset for its key (120 requests a minute). Ids are opaque strings like loc_3 (bare numbers still work in paths). Lists page with cursor: pass nextCursor back until it’s null (offset still works but is deprecated). GET /reviews/changes keeps a copy in sync. Every POST takes an Idempotency-Key. Webhooks are signed, retried with backoff, logged and replayable. An ef_test_ key reaches a sandbox with made-up data that makes no outside calls except its own webhooks, sent with livemode: false. TypeScript and Python clients generated from this spec are in the repo’s sdk/ folder.

The MCP server at /mcp also takes OAuth 2.1 access tokens (see mcpOAuth), which is how claude.ai connectors sign in. Those tokens only work at /mcp; the REST API takes keys.

Find a business on review sites by name.

POST /api/v1/place-searches

Search for a business createPlaceSearch

Starts a search for a business or app on each site that has a search source: Google, Tripadvisor and Trustpilot (through DataForSEO, when the server has it), the App Store and Google Play. Answers 202 at once; poll GET /place-searches/{id}. Each site retries with backoff until it’s done or has failed 5 times. Other sites are added by link. 30 searches an hour per workspace.

Needs a write key.

Parameters

NameInTypeAbout
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Body

FieldTypeAbout
querystringThe business or app name.
nearstring optionalA town or area, for Google and Tripadvisor.
sitesarray of string: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterra optionalDefaults to every searchable site.
{
  "query": "Harbour & Vine",
  "near": "Falmouth"
}

Responses

  • 202 Started. Location points at the search.
    FieldTypeAbout
    idintegerSearch id.
    querystringWhat was searched for.
    nearstring or null
    statusstring: running | done | faileddone once every site has finished (some may have failed); failed when all did.
    createdAtstringWhen it started.
    sitesarray of object
    candidatesarray of Candidate
    {
      "id": 55,
      "query": "Harbour & Vine",
      "near": "Falmouth",
      "status": "running",
      "createdAt": "2026-10-28T09:30:00.000Z",
      "sites": [
        {
          "site": "appstore",
          "status": "pending",
          "attempts": 0,
          "error": null,
          "finishedAt": null
        },
        {
          "site": "google",
          "status": "pending",
          "attempts": 0,
          "error": null,
          "finishedAt": null
        },
        {
          "site": "tripadvisor",
          "status": "pending",
          "attempts": 1,
          "error": null,
          "finishedAt": null
        }
      ],
      "candidates": []
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests

GET /api/v1/place-searches/{id}

Search status and results getPlaceSearch

Where each site’s search is, and the businesses found so far. Add one with POST /locations/{id}/profiles {candidateId}.

Any key (read or write).

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 200 The search.
    FieldTypeAbout
    idintegerSearch id.
    querystringWhat was searched for.
    nearstring or null
    statusstring: running | done | faileddone once every site has finished (some may have failed); failed when all did.
    createdAtstringWhen it started.
    sitesarray of object
    candidatesarray of Candidate
    {
      "id": 55,
      "query": "Harbour & Vine",
      "near": "Falmouth",
      "status": "done",
      "createdAt": "2026-10-28T09:30:00.000Z",
      "sites": [
        {
          "site": "appstore",
          "status": "done",
          "attempts": 0,
          "error": null,
          "finishedAt": "2026-10-28T09:30:00.000Z"
        },
        {
          "site": "google",
          "status": "done",
          "attempts": 0,
          "error": null,
          "finishedAt": "2026-10-28T09:30:00.000Z"
        },
        {
          "site": "tripadvisor",
          "status": "done",
          "attempts": 1,
          "error": null,
          "finishedAt": "2026-10-28T09:30:00.000Z"
        }
      ],
      "candidates": [
        {
          "candidateId": 301,
          "site": "google",
          "name": "Harbour & Vine",
          "address": "12 Arwenack St, Falmouth TR11 3JA",
          "url": "https://www.google.com/maps/place/Harbour+%26+Vine/@50.153,-5.067,17z?cid=1234567890123456789",
          "rating": 4.7,
          "reviewCount": 532
        },
        {
          "candidateId": 302,
          "site": "tripadvisor",
          "name": "Harbour & Vine",
          "address": null,
          "url": "https://www.tripadvisor.co.uk/Restaurant_Review-g186233-d1234567-Reviews-Harbour_Vine-Falmouth.html",
          "rating": 4.5,
          "reviewCount": 214
        }
      ]
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 404 Not Found
  • 429 Too Many Requests

locations

Businesses (or apps), each across its review sites.

GET /api/v1/locations

List locations listLocations

Every location in the workspace, oldest first. Locations past the plan’s limit stay listed but aren’t collected.

Any key (read or write).

Parameters

NameInTypeAbout
externalRef optionalquerystringOnly the location with your reference.

Responses

  • 200 The locations.
    FieldTypeAbout
    dataarray of Location
    {
      "data": [
        {
          "id": "loc_3",
          "name": "Harbour & Vine, Falmouth",
          "profileCount": 2,
          "createdAt": "2026-10-28T09:30:00.000Z",
          "externalRef": null
        }
      ]
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 429 Too Many Requests

POST /api/v1/locations

Add a location createLocation

Adds a business or branch. Refused with 402 once the plan’s locations are used up.

Needs a write key.

Parameters

NameInTypeAbout
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Body

FieldTypeAbout
namestring
externalRefstring or null optionalYour own reference for it, like your store id. Filter with GET /locations?externalRef=.
{
  "name": "Harbour & Vine, Falmouth",
  "externalRef": "store-0042"
}

Responses

  • 201 The new location.
    FieldTypeAbout
    idstringLocation id.
    namestringThe business or branch name.
    profileCountintegerHow many review pages this location has.
    createdAtstringWhen it was added.
    externalRefstring or nullYour own reference for it, like your store id. Filter with GET /locations?externalRef=.
    {
      "id": "loc_3",
      "name": "Harbour & Vine, Falmouth",
      "profileCount": 0,
      "createdAt": "2026-10-28T09:30:00.000Z",
      "externalRef": null
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 402 Payment Required
  • 403 Forbidden
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests

GET /api/v1/locations/{id}

Get a location getLocation

One location.

Any key (read or write).

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 200 The location.
    FieldTypeAbout
    idstringLocation id.
    namestringThe business or branch name.
    profileCountintegerHow many review pages this location has.
    createdAtstringWhen it was added.
    externalRefstring or nullYour own reference for it, like your store id. Filter with GET /locations?externalRef=.
    {
      "id": "loc_3",
      "name": "Harbour & Vine, Falmouth",
      "profileCount": 2,
      "createdAt": "2026-10-28T09:30:00.000Z",
      "externalRef": null
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 404 Not Found
  • 429 Too Many Requests

PATCH /api/v1/locations/{id}

Rename a location updateLocation

Renames a location.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Body

FieldTypeAbout
namestring optional
externalRefstring or null optionalYour own reference for it, like your store id. Filter with GET /locations?externalRef=.
{
  "name": "Harbour & Vine, Falmouth harbour",
  "externalRef": "store-0042"
}

Responses

  • 200 The renamed location.
    FieldTypeAbout
    idstringLocation id.
    namestringThe business or branch name.
    profileCountintegerHow many review pages this location has.
    createdAtstringWhen it was added.
    externalRefstring or nullYour own reference for it, like your store id. Filter with GET /locations?externalRef=.
    {
      "id": "loc_3",
      "name": "Harbour & Vine, Falmouth",
      "profileCount": 2,
      "createdAt": "2026-10-28T09:30:00.000Z",
      "externalRef": null
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 429 Too Many Requests

DELETE /api/v1/locations/{id}

Delete a location deleteLocation

Deletes a location with its review pages and their reviews. A workspace keeps at least one location (409).

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 204 Done; no body.
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 429 Too Many Requests

profiles

Review pages: one site of one location.

POST /api/v1/locations/{id}/profiles

Add a review page to a location addProfile

Adds a review page by its link (url) or by a place search result (candidateId), and queues its first collection. One page per site per location (409).

Needs a write key.

One profile per listing: a link to one that’s already here (another domain, tracking parameters, http) answers 409 with existingId.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Body

FieldTypeAbout
urlstring optionalThe review page link.
candidateIdinteger optionalA candidate from GET /place-searches/{id}.
namestring optionalDefaults to the site and business name.
{
  "candidateId": 302
}

Responses

  • 201 The new profile.
    FieldTypeAbout
    idstringProfile id.
    locationIdstringThe location it belongs to.
    locationNamestring or nullThat location’s name.
    sitestring: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterraWhich review site.
    namestringA name for the review page.
    urlstringThe review page.
    reviewCountintegerReviews held (not counting removed ones).
    averageRatingnumber or nullAverage star rating of those reviews.
    lastCheckedAtstring or nullWhen reviews were last collected.
    nextCheckAtstringWhen the next collection is due.
    lastErrorstring or nullWhy the last collection failed, if it did.
    createdAtstringWhen it was added.
    externalIdstring or nullThe listing’s own id on its site: a Google cid or place id, a Tripadvisor location id (d7106969), a Trustpilot domain, an App Store id, a Play package, a Booking.com hotel slug. Two links to one listing share it, so it’s what to match on, not url. null where the link doesn’t carry one.
    importobjectIts history import. The first reviews come within a minute or two; then older ones load in the background, up to the plan’s cap (Free 40, Plus 5,000). A profile.imported webhook says when it’s over.
    statusstring: queued | importing | done | failedqueued: first import not in yet. importing: loading older reviews. done or failed: over.
    reviewsintegerReviews held so far.
    limitinteger or nullThe history cap it ran to, once over.
    finishedAtstring or nullWhen it was over.
    errorstring or nullWhy it failed, if it did.
    {
      "id": "prf_12",
      "locationId": "loc_3",
      "locationName": "Harbour & Vine, Falmouth",
      "site": "tripadvisor",
      "name": "Tripadvisor: Harbour & Vine",
      "url": "https://www.tripadvisor.co.uk/Restaurant_Review-g186233-d1234567-Reviews-Harbour_Vine-Falmouth.html",
      "reviewCount": 0,
      "averageRating": null,
      "lastCheckedAt": null,
      "nextCheckAt": "2026-10-28T10:30:00.000Z",
      "lastError": null,
      "createdAt": "2026-10-01T12:00:00.000Z",
      "externalId": null,
      "import": {
        "status": "done",
        "reviews": 0,
        "limit": 5000,
        "finishedAt": null,
        "error": null
      }
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests

GET /api/v1/profiles

List profiles listProfiles

Every review page, or those of one location. Find a listing with site and externalId rather than its URL: links change, ids don’t.

Any key (read or write).

Parameters

NameInTypeAbout
location optionalquerystringOnly this location’s.
site optionalquerystringWith externalId: find a listing.
externalId optionalquerystringThe listing’s id on its site (see Profile.externalId), like d7106969 on Tripadvisor.

Responses

  • 200 The profiles.
    FieldTypeAbout
    dataarray of Profile
    {
      "data": [
        {
          "id": "prf_12",
          "locationId": "loc_3",
          "locationName": "Harbour & Vine, Falmouth",
          "site": "tripadvisor",
          "name": "Tripadvisor: Harbour & Vine",
          "url": "https://www.tripadvisor.co.uk/Restaurant_Review-g186233-d1234567-Reviews-Harbour_Vine-Falmouth.html",
          "reviewCount": 214,
          "averageRating": 4.6,
          "lastCheckedAt": "2026-10-28T09:30:00.000Z",
          "nextCheckAt": "2026-10-28T10:30:00.000Z",
          "lastError": null,
          "createdAt": "2026-10-01T12:00:00.000Z",
          "externalId": null,
          "import": {
            "status": "done",
            "reviews": 214,
            "limit": 5000,
            "finishedAt": "2026-10-28T09:30:00.000Z",
            "error": null
          }
        }
      ]
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 429 Too Many Requests

GET /api/v1/profiles/{id}

Get a profile getProfile

One review page.

Any key (read or write).

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 200 The profile.
    FieldTypeAbout
    idstringProfile id.
    locationIdstringThe location it belongs to.
    locationNamestring or nullThat location’s name.
    sitestring: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterraWhich review site.
    namestringA name for the review page.
    urlstringThe review page.
    reviewCountintegerReviews held (not counting removed ones).
    averageRatingnumber or nullAverage star rating of those reviews.
    lastCheckedAtstring or nullWhen reviews were last collected.
    nextCheckAtstringWhen the next collection is due.
    lastErrorstring or nullWhy the last collection failed, if it did.
    createdAtstringWhen it was added.
    externalIdstring or nullThe listing’s own id on its site: a Google cid or place id, a Tripadvisor location id (d7106969), a Trustpilot domain, an App Store id, a Play package, a Booking.com hotel slug. Two links to one listing share it, so it’s what to match on, not url. null where the link doesn’t carry one.
    importobjectIts history import. The first reviews come within a minute or two; then older ones load in the background, up to the plan’s cap (Free 40, Plus 5,000). A profile.imported webhook says when it’s over.
    statusstring: queued | importing | done | failedqueued: first import not in yet. importing: loading older reviews. done or failed: over.
    reviewsintegerReviews held so far.
    limitinteger or nullThe history cap it ran to, once over.
    finishedAtstring or nullWhen it was over.
    errorstring or nullWhy it failed, if it did.
    {
      "id": "prf_12",
      "locationId": "loc_3",
      "locationName": "Harbour & Vine, Falmouth",
      "site": "tripadvisor",
      "name": "Tripadvisor: Harbour & Vine",
      "url": "https://www.tripadvisor.co.uk/Restaurant_Review-g186233-d1234567-Reviews-Harbour_Vine-Falmouth.html",
      "reviewCount": 214,
      "averageRating": 4.6,
      "lastCheckedAt": "2026-10-28T09:30:00.000Z",
      "nextCheckAt": "2026-10-28T10:30:00.000Z",
      "lastError": null,
      "createdAt": "2026-10-01T12:00:00.000Z",
      "externalId": null,
      "import": {
        "status": "done",
        "reviews": 214,
        "limit": 5000,
        "finishedAt": "2026-10-28T09:30:00.000Z",
        "error": null
      }
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 404 Not Found
  • 429 Too Many Requests

DELETE /api/v1/profiles/{id}

Delete a profile deleteProfile

Deletes a review page and its reviews.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 204 Done; no body.
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 429 Too Many Requests

POST /api/v1/profiles/{id}/check

Collect now checkProfile

Queues a collection straight away instead of at the plan’s next check. At most 10 an hour per workspace (429). New reviews appear in a minute or two.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Responses

  • 202 Queued; nextCheckAt is now.
    FieldTypeAbout
    idstringProfile id.
    locationIdstringThe location it belongs to.
    locationNamestring or nullThat location’s name.
    sitestring: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterraWhich review site.
    namestringA name for the review page.
    urlstringThe review page.
    reviewCountintegerReviews held (not counting removed ones).
    averageRatingnumber or nullAverage star rating of those reviews.
    lastCheckedAtstring or nullWhen reviews were last collected.
    nextCheckAtstringWhen the next collection is due.
    lastErrorstring or nullWhy the last collection failed, if it did.
    createdAtstringWhen it was added.
    externalIdstring or nullThe listing’s own id on its site: a Google cid or place id, a Tripadvisor location id (d7106969), a Trustpilot domain, an App Store id, a Play package, a Booking.com hotel slug. Two links to one listing share it, so it’s what to match on, not url. null where the link doesn’t carry one.
    importobjectIts history import. The first reviews come within a minute or two; then older ones load in the background, up to the plan’s cap (Free 40, Plus 5,000). A profile.imported webhook says when it’s over.
    statusstring: queued | importing | done | failedqueued: first import not in yet. importing: loading older reviews. done or failed: over.
    reviewsintegerReviews held so far.
    limitinteger or nullThe history cap it ran to, once over.
    finishedAtstring or nullWhen it was over.
    errorstring or nullWhy it failed, if it did.
    {
      "id": "prf_12",
      "locationId": "loc_3",
      "locationName": "Harbour & Vine, Falmouth",
      "site": "tripadvisor",
      "name": "Tripadvisor: Harbour & Vine",
      "url": "https://www.tripadvisor.co.uk/Restaurant_Review-g186233-d1234567-Reviews-Harbour_Vine-Falmouth.html",
      "reviewCount": 214,
      "averageRating": 4.6,
      "lastCheckedAt": "2026-10-28T09:30:00.000Z",
      "nextCheckAt": "2026-10-28T09:30:00.000Z",
      "lastError": null,
      "createdAt": "2026-10-01T12:00:00.000Z",
      "externalId": null,
      "import": {
        "status": "done",
        "reviews": 214,
        "limit": 5000,
        "finishedAt": "2026-10-28T09:30:00.000Z",
        "error": null
      }
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests

reviews

Collected reviews.

GET /api/v1/reviews

List reviews listReviews

Reviews newest first, filtered like the inbox. Removed reviews are left out unless status is deleted or any.

Any key (read or write).

Page with cursor: pass each answer’s nextCursor back until it’s null. limit and offset still work but offset is deprecated. To keep a copy in sync, use GET /reviews/changes instead.

Parameters

NameInTypeAbout
site optionalquerystring: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterra
location optionalquerystring
profile optionalquerystring
rating optionalqueryinteger
status optionalquerystring: unreplied | replied | open | handled | edited | deleted | any
from optionalquerystringPublished on or after.
to optionalquerystringPublished on or before.
since optionalquerystringFirst seen by Earful at or after; for polling.
q optionalquerystringText, title or reviewer contains.
tag optionalquerystring
Limit optionalqueryintegerPage size.
Offset optionalqueryintegerDeprecated: page with cursor. Rows to skip.
Cursor optionalquerystringnextCursor from the previous page. Keep the same filters.
sentiment optionalquerystring: positive | neutral | negative | mixedOnly reviews the AI read this way.
version optionalquerystringApp stores: reviews left on this app version.
country optionalquerystringApp stores: reviews from this store country.

Responses

  • 200 A page of reviews.
    FieldTypeAbout
    dataarray of Review
    totalintegerMatching reviews.
    limitinteger
    offsetinteger
    nextCursorstring or nullThe next page’s cursor; null after the last review.
    {
      "data": [
        {
          "id": "rev_981",
          "site": "tripadvisor",
          "profileId": "prf_12",
          "profileName": "Tripadvisor: Harbour & Vine",
          "reviewId": "1011625097",
          "author": "Ann P",
          "rating": 2,
          "title": "Slow on a Saturday",
          "text": "Lovely crab linguine but we waited 50 minutes for mains.",
          "language": "en",
          "publishedAt": "2026-10-27T19:04:00.000Z",
          "updatedAt": null,
          "ownerReply": null,
          "reviewUrl": "https://www.tripadvisor.co.uk/ShowUserReviews-g186233-d1234567-r1011625097-Harbour_Vine-Falmouth.html",
          "editedAt": null,
          "deletedAt": null,
          "handledAt": null,
          "firstSeenAt": "2026-10-28T09:30:00.000Z",
          "tags": [
            "service"
          ],
          "draft": null,
          "sentiment": "negative",
          "translation": null,
          "appVersion": null,
          "country": null,
          "history": false
        }
      ],
      "total": 1,
      "limit": 50,
      "offset": 0,
      "nextCursor": null
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 429 Too Many Requests

GET /api/v1/reviews/{id}

Get a review getReview

One review, with how a reply to it would reach the site.

Any key (read or write).

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 200 The review.
    FieldTypeAbout
    idstring optionalEarful’s id for the review.
    sitestring: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterra optional
    profileIdstring optionalThe profile it came from.
    profileNamestring or null optional
    reviewIdstring optionalThe site’s own id for the review.
    authorstring or null optional
    ratinginteger or null optionalWhole stars; out-of-10 sites are halved.
    titlestring or null optional
    textstring or null optional
    languagestring or null optional
    publishedAtstring or null optionalWhen the reviewer posted it.
    updatedAtstring or null optionalWhen the site says it was last changed.
    ownerReplynull or object optionalThe business’s reply, from the site or recorded in Earful.
    reviewUrlstring or null optional
    editedAtstring or null optionalWhen Earful noticed the reviewer edit it.
    deletedAtstring or null optionalWhen Earful noticed it removed from the site.
    handledAtstring or null optionalWhen it was marked handled.
    firstSeenAtstring optionalWhen Earful first collected it.
    tagsarray of string optional
    draftstring or null optionalThe reply draft, if there is one.
    sentimentstring or null: positive | neutral | negative | mixed optionalRead by AI when the review arrived; null until then or with AI off.
    translationstring or null optionalAn English translation by AI, for reviews in another language.
    appVersionstring or null optionalApp stores: the app version the review was left on.
    countrystring or null optionalApp stores: the store country, ISO 3166 (alpha-2 from public feeds, alpha-3 from App Store Connect).
    historyboolean optionalCame in with the profile’s first import or history backfill, so no alert went out for it.
    replyRouteobject optional
    modestring: api | link | noneapi: POST /reply posts to the site. link: post it on the site at url. none: no way to reply.
    viastring or nullWhich owner API posts it (appstore, googleplay, gbp) when mode is api.
    urlstring or nullThe site’s reply page when mode is link.
    {
      "id": "rev_981",
      "site": "tripadvisor",
      "profileId": "prf_12",
      "profileName": "Tripadvisor: Harbour & Vine",
      "reviewId": "1011625097",
      "author": "Ann P",
      "rating": 2,
      "title": "Slow on a Saturday",
      "text": "Lovely crab linguine but we waited 50 minutes for mains.",
      "language": "en",
      "publishedAt": "2026-10-27T19:04:00.000Z",
      "updatedAt": null,
      "ownerReply": null,
      "reviewUrl": "https://www.tripadvisor.co.uk/ShowUserReviews-g186233-d1234567-r1011625097-Harbour_Vine-Falmouth.html",
      "editedAt": null,
      "deletedAt": null,
      "handledAt": null,
      "firstSeenAt": "2026-10-28T09:30:00.000Z",
      "tags": [
        "service"
      ],
      "draft": null,
      "replyRoute": {
        "mode": "link",
        "via": null,
        "url": "https://www.tripadvisor.co.uk/ManagementCenter"
      },
      "sentiment": "negative",
      "translation": null,
      "appVersion": null,
      "country": null,
      "history": false
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 404 Not Found
  • 429 Too Many Requests

PATCH /api/v1/reviews/{id}

Mark handled, set tags updateReview

Marks a review handled or not, and replaces its tags.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Body

FieldTypeAbout
handledboolean optional
tagsarray of string optional
{
  "handled": true,
  "tags": [
    "service",
    "weekend"
  ]
}

Responses

  • 200 The review.
    FieldTypeAbout
    idstring optionalEarful’s id for the review.
    sitestring: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterra optional
    profileIdstring optionalThe profile it came from.
    profileNamestring or null optional
    reviewIdstring optionalThe site’s own id for the review.
    authorstring or null optional
    ratinginteger or null optionalWhole stars; out-of-10 sites are halved.
    titlestring or null optional
    textstring or null optional
    languagestring or null optional
    publishedAtstring or null optionalWhen the reviewer posted it.
    updatedAtstring or null optionalWhen the site says it was last changed.
    ownerReplynull or object optionalThe business’s reply, from the site or recorded in Earful.
    reviewUrlstring or null optional
    editedAtstring or null optionalWhen Earful noticed the reviewer edit it.
    deletedAtstring or null optionalWhen Earful noticed it removed from the site.
    handledAtstring or null optionalWhen it was marked handled.
    firstSeenAtstring optionalWhen Earful first collected it.
    tagsarray of string optional
    draftstring or null optionalThe reply draft, if there is one.
    sentimentstring or null: positive | neutral | negative | mixed optionalRead by AI when the review arrived; null until then or with AI off.
    translationstring or null optionalAn English translation by AI, for reviews in another language.
    appVersionstring or null optionalApp stores: the app version the review was left on.
    countrystring or null optionalApp stores: the store country, ISO 3166 (alpha-2 from public feeds, alpha-3 from App Store Connect).
    historyboolean optionalCame in with the profile’s first import or history backfill, so no alert went out for it.
    replyRouteobject optional
    modestring: api | link | noneapi: POST /reply posts to the site. link: post it on the site at url. none: no way to reply.
    viastring or nullWhich owner API posts it (appstore, googleplay, gbp) when mode is api.
    urlstring or nullThe site’s reply page when mode is link.
    {
      "id": "rev_981",
      "site": "tripadvisor",
      "profileId": "prf_12",
      "profileName": "Tripadvisor: Harbour & Vine",
      "reviewId": "1011625097",
      "author": "Ann P",
      "rating": 2,
      "title": "Slow on a Saturday",
      "text": "Lovely crab linguine but we waited 50 minutes for mains.",
      "language": "en",
      "publishedAt": "2026-10-27T19:04:00.000Z",
      "updatedAt": null,
      "ownerReply": null,
      "reviewUrl": "https://www.tripadvisor.co.uk/ShowUserReviews-g186233-d1234567-r1011625097-Harbour_Vine-Falmouth.html",
      "editedAt": null,
      "deletedAt": null,
      "handledAt": "2026-10-28T09:30:00.000Z",
      "firstSeenAt": "2026-10-28T09:30:00.000Z",
      "tags": [
        "service",
        "weekend"
      ],
      "draft": null,
      "replyRoute": {
        "mode": "link",
        "via": null,
        "url": "https://www.tripadvisor.co.uk/ManagementCenter"
      },
      "sentiment": "negative",
      "translation": null,
      "appVersion": null,
      "country": null,
      "history": false
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 429 Too Many Requests

POST /api/v1/reviews/{id}/analyse

Read a review with AI analyseReview

Reads the review again: sentiment, an English translation if it’s in another language, and the workspace’s AI tags that fit. New reviews get this automatically; use it for older ones.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Responses

  • 200 The review, read.
    FieldTypeAbout
    idstring optionalEarful’s id for the review.
    sitestring: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterra optional
    profileIdstring optionalThe profile it came from.
    profileNamestring or null optional
    reviewIdstring optionalThe site’s own id for the review.
    authorstring or null optional
    ratinginteger or null optionalWhole stars; out-of-10 sites are halved.
    titlestring or null optional
    textstring or null optional
    languagestring or null optional
    publishedAtstring or null optionalWhen the reviewer posted it.
    updatedAtstring or null optionalWhen the site says it was last changed.
    ownerReplynull or object optionalThe business’s reply, from the site or recorded in Earful.
    reviewUrlstring or null optional
    editedAtstring or null optionalWhen Earful noticed the reviewer edit it.
    deletedAtstring or null optionalWhen Earful noticed it removed from the site.
    handledAtstring or null optionalWhen it was marked handled.
    firstSeenAtstring optionalWhen Earful first collected it.
    tagsarray of string optional
    draftstring or null optionalThe reply draft, if there is one.
    sentimentstring or null: positive | neutral | negative | mixed optionalRead by AI when the review arrived; null until then or with AI off.
    translationstring or null optionalAn English translation by AI, for reviews in another language.
    appVersionstring or null optionalApp stores: the app version the review was left on.
    countrystring or null optionalApp stores: the store country, ISO 3166 (alpha-2 from public feeds, alpha-3 from App Store Connect).
    historyboolean optionalCame in with the profile’s first import or history backfill, so no alert went out for it.
    replyRouteobject optional
    modestring: api | link | noneapi: POST /reply posts to the site. link: post it on the site at url. none: no way to reply.
    viastring or nullWhich owner API posts it (appstore, googleplay, gbp) when mode is api.
    urlstring or nullThe site’s reply page when mode is link.
    {
      "id": "rev_981",
      "site": "tripadvisor",
      "profileId": "prf_12",
      "profileName": "Tripadvisor: Harbour & Vine",
      "reviewId": "1011625097",
      "author": "Ann P",
      "rating": 2,
      "title": "Slow on a Saturday",
      "text": "Lovely crab linguine but we waited 50 minutes for mains.",
      "language": "en",
      "publishedAt": "2026-10-27T19:04:00.000Z",
      "updatedAt": null,
      "ownerReply": null,
      "reviewUrl": "https://www.tripadvisor.co.uk/ShowUserReviews-g186233-d1234567-r1011625097-Harbour_Vine-Falmouth.html",
      "editedAt": null,
      "deletedAt": null,
      "handledAt": null,
      "firstSeenAt": "2026-10-28T09:30:00.000Z",
      "tags": [
        "service"
      ],
      "draft": null,
      "replyRoute": {
        "mode": "link",
        "via": null,
        "url": "https://www.tripadvisor.co.uk/ManagementCenter"
      },
      "sentiment": "negative",
      "translation": null,
      "appVersion": null,
      "country": null,
      "history": false
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests
  • 503 Service Unavailable

GET /api/v1/tag-rules

List AI tags listTagRules

Tags the AI applies to new reviews, each with when it applies.

Any key (read or write).

Responses

  • 200 The AI tags.
    FieldTypeAbout
    dataarray of TagRule
    {
      "data": [
        {
          "id": 3,
          "tag": "service",
          "description": "Mentions waiting, slow service or staff attitude",
          "createdAt": "2026-10-28T09:30:00.000Z"
        }
      ]
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 429 Too Many Requests

POST /api/v1/tag-rules

Add an AI tag createTagRule

Adds a tag the AI applies to new reviews when its description fits; saving an existing tag changes its description. Up to 50.

Needs a write key.

Parameters

NameInTypeAbout
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Body

FieldTypeAbout
tagstring
descriptionstring
{
  "tag": "service",
  "description": "Mentions waiting, slow service or staff attitude"
}

Responses

  • 201 The AI tag.
    FieldTypeAbout
    idinteger
    tagstringLower case letters, digits, - and _.
    descriptionstringWhen the AI should apply it.
    createdAtstring
    {
      "id": 3,
      "tag": "service",
      "description": "Mentions waiting, slow service or staff attitude",
      "createdAt": "2026-10-28T09:30:00.000Z"
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests

DELETE /api/v1/tag-rules/{id}

Remove an AI tag deleteTagRule

Stops the AI applying it. Reviews keep tags they already have.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 204 Done; no body.
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 429 Too Many Requests

GET /api/v1/reviews/changes

Sync reviews listReviewChanges

Every review that changed after the cursor (new, edited, replied, tagged, handled, removed from the site), oldest change first, and the ids of reviews that are gone for good (their profile was deleted). Start without a cursor to walk the whole workspace, then keep nextCursor and call again later: you get only what changed. Apply removed, then data.

Any key (read or write).

Parameters

NameInTypeAbout
Cursor optionalquerystringnextCursor from the previous page. Keep the same filters.
limit optionalqueryintegerChanges per page.

Responses

  • 200 Changes after the cursor.
    FieldTypeAbout
    dataarray of ReviewReviews as they are now.
    removedarray of stringIds of reviews deleted from Earful.
    nextCursorstringWhere to carry on from; always set.
    hasMorebooleanMore changes are waiting: call again now.
    {
      "data": [
        {
          "id": "rev_981",
          "site": "tripadvisor",
          "profileId": "prf_12",
          "profileName": "Tripadvisor: Harbour & Vine",
          "reviewId": "1011625097",
          "author": "Ann P",
          "rating": 2,
          "title": "Slow on a Saturday",
          "text": "Lovely crab linguine but we waited 50 minutes for mains.",
          "language": "en",
          "publishedAt": "2026-10-27T19:04:00.000Z",
          "updatedAt": null,
          "ownerReply": null,
          "reviewUrl": "https://www.tripadvisor.co.uk/ShowUserReviews-g186233-d1234567-r1011625097-Harbour_Vine-Falmouth.html",
          "editedAt": null,
          "deletedAt": null,
          "handledAt": null,
          "firstSeenAt": "2026-10-28T09:30:00.000Z",
          "tags": [
            "service"
          ],
          "draft": null,
          "sentiment": "negative",
          "translation": null,
          "appVersion": null,
          "country": null,
          "history": false
        }
      ],
      "removed": [
        "rev_77"
      ],
      "nextCursor": "eyJzZXEiOjQwMjF9",
      "hasMore": false
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 429 Too Many Requests

replies

AI drafts and replies.

POST /api/v1/reviews/{id}/draft

Draft a reply with AI generateDraft

Writes a reply in the workspace’s voice and keeps it as the review’s draft. Free includes 10 a month (402 after).

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Responses

  • 201 The draft.
    FieldTypeAbout
    textstringThe draft reply.
    aiDraftsLeftinteger or nullAI drafts left this month; null when unlimited or not an AI draft.
    {
      "text": "Hi Ann, thank you, and sorry about the wait…",
      "aiDraftsLeft": 7
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 402 Payment Required
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests
  • 503 Service Unavailable

PUT /api/v1/reviews/{id}/draft

Edit the draft saveDraft

Replaces the review’s draft reply.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Body

FieldTypeAbout
textstring
{
  "text": "Hi Ann, thank you, and sorry about the wait on Saturday. Sam"
}

Responses

  • 200 The saved draft.
    FieldTypeAbout
    textstringThe draft reply.
    aiDraftsLeftinteger or nullAI drafts left this month; null when unlimited or not an AI draft.
    {
      "text": "Hi Ann, thank you, and sorry about the wait on Saturday. Sam",
      "aiDraftsLeft": null
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 429 Too Many Requests

POST /api/v1/reviews/{id}/reply

Reply replyToReview

Posts the reply through the site’s owner API where the profile is connected to one (App Store Connect, Google Play, Google Business Profile) and the plan allows direct replies. Otherwise Earful records it as replied by hand: postedToSite is false and replyUrl is where to post it.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Body

FieldTypeAbout
textstring
{
  "text": "Hi Ann, thank you, and sorry about the wait on Saturday. Sam"
}

Responses

  • 201 Posted or recorded.
    FieldTypeAbout
    postedToSitebooleantrue when Earful posted it through the site’s owner API.
    viastring: appstore | googleplay | gbp | facebook | manualmanual means recorded only: post it on the site yourself.
    replyUrlstring or nullWhere to post it when via is manual.
    reviewReviewDetail
    {
      "postedToSite": false,
      "via": "manual",
      "replyUrl": "https://www.tripadvisor.co.uk/ManagementCenter",
      "review": {
        "id": "rev_981",
        "site": "tripadvisor",
        "profileId": "prf_12",
        "profileName": "Tripadvisor: Harbour & Vine",
        "reviewId": "1011625097",
        "author": "Ann P",
        "rating": 2,
        "title": "Slow on a Saturday",
        "text": "Lovely crab linguine but we waited 50 minutes for mains.",
        "language": "en",
        "publishedAt": "2026-10-27T19:04:00.000Z",
        "updatedAt": null,
        "ownerReply": null,
        "reviewUrl": "https://www.tripadvisor.co.uk/ShowUserReviews-g186233-d1234567-r1011625097-Harbour_Vine-Falmouth.html",
        "editedAt": null,
        "deletedAt": null,
        "handledAt": null,
        "firstSeenAt": "2026-10-28T09:30:00.000Z",
        "tags": [
          "service"
        ],
        "draft": null,
        "replyRoute": {
          "mode": "link",
          "via": null,
          "url": "https://www.tripadvisor.co.uk/ManagementCenter"
        },
        "sentiment": "negative",
        "translation": null,
        "appVersion": null,
        "country": null,
        "history": false
      }
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests

auto-replies

Rules, the approval queue and the log.

GET /api/v1/reply-rules

List reply rules listReplyRules

Auto-reply rules, in the order they’re tried: a location’s own first, then the ones for every location. The first that fits a new review wins.

Any key (read or write).

Responses

  • 200 The rules.
    FieldTypeAbout
    dataarray of ReplyRule
    {
      "data": [
        {
          "id": 2,
          "locationId": null,
          "minRating": 4,
          "maxRating": 5,
          "sites": [
            "appstore",
            "googleplay"
          ],
          "languages": [],
          "keywords": [],
          "action": "post",
          "createdAt": "2026-10-28T09:30:00.000Z"
        }
      ]
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 429 Too Many Requests

POST /api/v1/reply-rules

Add a reply rule createReplyRule

New reviews that fit get an AI reply that is posted (action post, on sites with a reply API, up to the daily cap) or held in the approval queue. Defaults: 4 and 5 stars, held for approval. Auto-replies also need turning on: PATCH /api/v1/workspace with autoReply.

Needs a write key.

Parameters

NameInTypeAbout
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Body

FieldTypeAbout
actionstring: post | approve optional
locationIdstring or integer optionalOnly this location. Its id (loc_3), or the bare number.
minRatinginteger optional1 to 3 stars only when you set it lower.
maxRatinginteger optional
sitesarray of string: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterra optional
languagesarray of string optional
keywordsarray of string optional
{
  "action": "post",
  "minRating": 4,
  "sites": [
    "appstore",
    "googleplay"
  ]
}

Responses

  • 201 The rule.
    FieldTypeAbout
    idinteger
    locationIdstring or nullThe location it covers; null for every location.
    minRatinginteger
    maxRatinginteger
    sitesarray of string: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterraEmpty for every site.
    languagesarray of stringTwo-letter codes; empty for any.
    keywordsarray of stringThe review must mention one; empty for any.
    actionstring: post | approvepost: post the AI reply where the site has a reply API, otherwise hold it. approve: always hold it.
    createdAtstring
    {
      "id": 2,
      "locationId": null,
      "minRating": 4,
      "maxRating": 5,
      "sites": [
        "appstore",
        "googleplay"
      ],
      "languages": [],
      "keywords": [],
      "action": "post",
      "createdAt": "2026-10-28T09:30:00.000Z"
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests

GET /api/v1/reply-rules/{id}

Get a reply rule getReplyRule

One auto-reply rule.

Any key (read or write).

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 200 The rule.
    FieldTypeAbout
    idinteger
    locationIdstring or nullThe location it covers; null for every location.
    minRatinginteger
    maxRatinginteger
    sitesarray of string: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterraEmpty for every site.
    languagesarray of stringTwo-letter codes; empty for any.
    keywordsarray of stringThe review must mention one; empty for any.
    actionstring: post | approvepost: post the AI reply where the site has a reply API, otherwise hold it. approve: always hold it.
    createdAtstring
    {
      "id": 2,
      "locationId": null,
      "minRating": 4,
      "maxRating": 5,
      "sites": [
        "appstore",
        "googleplay"
      ],
      "languages": [],
      "keywords": [],
      "action": "post",
      "createdAt": "2026-10-28T09:30:00.000Z"
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 404 Not Found
  • 429 Too Many Requests

DELETE /api/v1/reply-rules/{id}

Remove a reply rule deleteReplyRule

Removes the rule. Replies it already held stay in the queue.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 204 Done; no body.
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 429 Too Many Requests

GET /api/v1/auto-replies

List auto-replies listAutoReplies

What auto-replies did, newest first: the approval queue (status pending) and the audit log of what was posted, approved, rejected or skipped.

Any key (read or write).

Parameters

NameInTypeAbout
status optionalquerystring: pending | posted | approved | rejected | skipped
Limit optionalqueryintegerPage size.
Offset optionalqueryintegerDeprecated: page with cursor. Rows to skip.
Cursor optionalquerystringnextCursor from the previous page. Keep the same filters.

Responses

  • 200 Auto-replies.
    FieldTypeAbout
    dataarray of AutoReply
    limitinteger
    offsetinteger
    nextCursorstring or nullThe next page’s cursor; null after the last item.
    {
      "data": [
        {
          "id": "arp_14",
          "reviewId": "rev_981",
          "ruleId": 2,
          "status": "pending",
          "text": "Thanks so much, Ann! We’re glad the crab linguine hit the spot.",
          "reason": "The rule holds replies for approval.",
          "decidedBy": null,
          "createdAt": "2026-10-28T09:30:00.000Z",
          "decidedAt": null,
          "review": {
            "site": "appstore",
            "rating": 5,
            "author": "Ann P",
            "text": "Lovely app, booking took seconds.",
            "profileName": "App Store: Harbour & Vine"
          }
        }
      ],
      "limit": 50,
      "offset": 0,
      "nextCursor": null
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 429 Too Many Requests

GET /api/v1/auto-replies/{id}

Get an auto-reply getAutoReply

One auto-reply.

Any key (read or write).

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 200 The auto-reply.
    FieldTypeAbout
    idstringAuto-reply id.
    reviewIdstringThe review.
    ruleIdinteger or null
    statusstring: pending | posted | approved | rejected | skippedpending is the approval queue; posted means posted automatically.
    textstring or nullThe AI reply (as approved, if edited).
    reasonstring or nullWhy it’s held or skipped.
    decidedBystring or nullauto, an email, an API key or a Slack user.
    createdAtstring
    decidedAtstring or null
    reviewobject
    sitestring: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterra
    ratinginteger or null
    authorstring or null
    textstring or null
    profileNamestring
    {
      "id": "arp_14",
      "reviewId": "rev_981",
      "ruleId": 2,
      "status": "pending",
      "text": "Thanks so much, Ann! We’re glad the crab linguine hit the spot.",
      "reason": "The rule holds replies for approval.",
      "decidedBy": null,
      "createdAt": "2026-10-28T09:30:00.000Z",
      "decidedAt": null,
      "review": {
        "site": "appstore",
        "rating": 5,
        "author": "Ann P",
        "text": "Lovely app, booking took seconds.",
        "profileName": "App Store: Harbour & Vine"
      }
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 404 Not Found
  • 429 Too Many Requests

POST /api/v1/auto-replies/{id}/approve

Approve and post approveAutoReply

Posts a held reply to the site, as drafted or with your text. 409 if it was already decided, the review has a reply, or the site has no reply API (copy those across).

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Body

FieldTypeAbout
textstring optionalLeave out to post the AI draft.
{}

Responses

  • 200 The auto-reply, approved.
    FieldTypeAbout
    idstringAuto-reply id.
    reviewIdstringThe review.
    ruleIdinteger or null
    statusstring: pending | posted | approved | rejected | skippedpending is the approval queue; posted means posted automatically.
    textstring or nullThe AI reply (as approved, if edited).
    reasonstring or nullWhy it’s held or skipped.
    decidedBystring or nullauto, an email, an API key or a Slack user.
    createdAtstring
    decidedAtstring or null
    reviewobject
    sitestring: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterra
    ratinginteger or null
    authorstring or null
    textstring or null
    profileNamestring
    {
      "id": "arp_14",
      "reviewId": "rev_981",
      "ruleId": 2,
      "status": "approved",
      "text": "Thanks so much, Ann! We’re glad the crab linguine hit the spot.",
      "reason": null,
      "decidedBy": "api:Zapier",
      "createdAt": "2026-10-28T09:30:00.000Z",
      "decidedAt": "2026-10-28T09:30:00.000Z",
      "review": {
        "site": "appstore",
        "rating": 5,
        "author": "Ann P",
        "text": "Lovely app, booking took seconds.",
        "profileName": "App Store: Harbour & Vine"
      }
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 402 Payment Required
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests

POST /api/v1/auto-replies/{id}/reject

Reject rejectAutoReply

Takes a held reply out of the queue without posting it. The draft stays on the review.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Responses

  • 200 The auto-reply, rejected.
    FieldTypeAbout
    idstringAuto-reply id.
    reviewIdstringThe review.
    ruleIdinteger or null
    statusstring: pending | posted | approved | rejected | skippedpending is the approval queue; posted means posted automatically.
    textstring or nullThe AI reply (as approved, if edited).
    reasonstring or nullWhy it’s held or skipped.
    decidedBystring or nullauto, an email, an API key or a Slack user.
    createdAtstring
    decidedAtstring or null
    reviewobject
    sitestring: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterra
    ratinginteger or null
    authorstring or null
    textstring or null
    profileNamestring
    {
      "id": "arp_14",
      "reviewId": "rev_981",
      "ruleId": 2,
      "status": "rejected",
      "text": "Thanks so much, Ann! We’re glad the crab linguine hit the spot.",
      "reason": "The rule holds replies for approval.",
      "decidedBy": "api:Zapier",
      "createdAt": "2026-10-28T09:30:00.000Z",
      "decidedAt": "2026-10-28T09:30:00.000Z",
      "review": {
        "site": "appstore",
        "rating": 5,
        "author": "Ann P",
        "text": "Lovely app, booking took seconds.",
        "profileName": "App Store: Harbour & Vine"
      }
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests

alerts

Email, Slack and webhooks for new reviews.

GET /api/v1/alerts

List alerts listAlerts

Every alert: email digests, instant email, Slack and signed webhooks.

Any key (read or write).

Responses

  • 200 The alerts.
    FieldTypeAbout
    dataarray of Alert
    {
      "data": [
        {
          "id": "alr_7",
          "channel": "webhook",
          "frequency": null,
          "target": "hooks.acme.example",
          "maxRating": 3,
          "sites": [],
          "lastSentAt": "2026-10-28T09:30:00.000Z",
          "lastError": null,
          "createdAt": "2026-10-28T09:30:00.000Z",
          "locationId": null,
          "sentiments": [],
          "tags": [],
          "keywords": []
        }
      ]
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 429 Too Many Requests

POST /api/v1/alerts

Add an alert or webhook createAlert

Adds an alert. A webhook gets a POST of {event: "reviews.new", workspace, reviews: [Review]} for each batch of new reviews, signed with an Earful-Signature: t=…,v1=… header: an HMAC-SHA256 of "t.body" with the secret in this answer. Filters (rating, sites, location, sentiment, tags, keywords) all have to match. Email and Slack alerts carry a signed link to approve or edit the reply without signing in. Slack and webhooks are on Plus (402).

Needs a write key.

Parameters

NameInTypeAbout
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Body

FieldTypeAbout
channelstring: digest | email | slack | webhook
targetstringAn email address, a Slack incoming webhook URL or your https webhook URL.
frequencystring: daily | weekly optionalDigests only.
maxRatinginteger: 1 | 2 | 3 | 4 optional
sitesarray of string: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterra optional
locationIdstring or integer optionalOnly this location. Its id (loc_3), or the bare number.
sentimentsarray of string: positive | neutral | negative | mixed optional
tagsarray of string optional
keywordsarray of string optionalMatch any one, case-insensitive.
{
  "channel": "webhook",
  "target": "https://hooks.acme.example/earful",
  "maxRating": 3
}

Responses

  • 201 The alert, and a webhook’s secret.
    FieldTypeAbout
    idstring optionalAlert id.
    channelstring: digest | email | slack | webhook optional
    frequencystring or null: daily | weekly optionalFor digests.
    targetstring optionalWhere it goes: the email address, “Slack channel”, or the webhook’s host. Full URLs stay encrypted.
    maxRatinginteger or null optionalOnly reviews at or below this rating; null for all.
    sitesarray of string: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterra optionalOnly these sites; empty for all.
    lastSentAtstring or null optionalWhen it last sent.
    lastErrorstring or null optional
    createdAtstring optionalWhen it was added.
    locationIdstring or null optionalOnly this location; null for all.
    sentimentsarray of string: positive | neutral | negative | mixed optionalOnly reviews the AI read as one of these; empty for any.
    tagsarray of string optionalOnly reviews with one of these tags; empty for any.
    keywordsarray of string optionalOnly reviews that mention one of these (in the text or its translation); empty for any.
    secretstring or null optionalA webhook’s signing secret. Shown only in this answer.
    {
      "id": "alr_7",
      "channel": "webhook",
      "frequency": null,
      "target": "hooks.acme.example",
      "maxRating": 3,
      "sites": [],
      "lastSentAt": "2026-10-28T09:30:00.000Z",
      "lastError": null,
      "createdAt": "2026-10-28T09:30:00.000Z",
      "secret": "whsec_9fQ2…",
      "locationId": null,
      "sentiments": [],
      "tags": [],
      "keywords": []
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 402 Payment Required
  • 403 Forbidden
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests

GET /api/v1/alerts/{id}

Get an alert getAlert

One alert.

Any key (read or write).

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 200 The alert.
    FieldTypeAbout
    idstringAlert id.
    channelstring: digest | email | slack | webhook
    frequencystring or null: daily | weeklyFor digests.
    targetstringWhere it goes: the email address, “Slack channel”, or the webhook’s host. Full URLs stay encrypted.
    maxRatinginteger or nullOnly reviews at or below this rating; null for all.
    sitesarray of string: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterraOnly these sites; empty for all.
    lastSentAtstring or nullWhen it last sent.
    lastErrorstring or null
    createdAtstringWhen it was added.
    locationIdstring or nullOnly this location; null for all.
    sentimentsarray of string: positive | neutral | negative | mixedOnly reviews the AI read as one of these; empty for any.
    tagsarray of stringOnly reviews with one of these tags; empty for any.
    keywordsarray of stringOnly reviews that mention one of these (in the text or its translation); empty for any.
    {
      "id": "alr_7",
      "channel": "webhook",
      "frequency": null,
      "target": "hooks.acme.example",
      "maxRating": 3,
      "sites": [],
      "lastSentAt": "2026-10-28T09:30:00.000Z",
      "lastError": null,
      "createdAt": "2026-10-28T09:30:00.000Z",
      "locationId": null,
      "sentiments": [],
      "tags": [],
      "keywords": []
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 404 Not Found
  • 429 Too Many Requests

DELETE /api/v1/alerts/{id}

Delete an alert deleteAlert

Stops an alert.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 204 Done; no body.
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 429 Too Many Requests

POST /api/v1/alerts/{id}/test

Send a test testAlert

Sends the newest review (or a made-up one) through the alert now. 400 says why delivery failed.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Responses

  • 200 Sent.
    FieldTypeAbout
    idstringAlert id.
    channelstring: digest | email | slack | webhook
    frequencystring or null: daily | weeklyFor digests.
    targetstringWhere it goes: the email address, “Slack channel”, or the webhook’s host. Full URLs stay encrypted.
    maxRatinginteger or nullOnly reviews at or below this rating; null for all.
    sitesarray of string: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterraOnly these sites; empty for all.
    lastSentAtstring or nullWhen it last sent.
    lastErrorstring or null
    createdAtstringWhen it was added.
    locationIdstring or nullOnly this location; null for all.
    sentimentsarray of string: positive | neutral | negative | mixedOnly reviews the AI read as one of these; empty for any.
    tagsarray of stringOnly reviews with one of these tags; empty for any.
    keywordsarray of stringOnly reviews that mention one of these (in the text or its translation); empty for any.
    {
      "id": "alr_7",
      "channel": "webhook",
      "frequency": null,
      "target": "hooks.acme.example",
      "maxRating": 3,
      "sites": [],
      "lastSentAt": "2026-10-28T09:30:00.000Z",
      "lastError": null,
      "createdAt": "2026-10-28T09:30:00.000Z",
      "locationId": null,
      "sentiments": [],
      "tags": [],
      "keywords": []
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests

GET /api/v1/webhook-deliveries

List webhook deliveries listWebhookDeliveries

Every webhook send, newest first: its status, tries and your endpoint’s answer. A failed send is retried after 1, 5, 30, 120, 360 and 720 minutes, then given up. Kept 30 days.

Any key (read or write).

Parameters

NameInTypeAbout
alert optionalquerystringOnly this webhook alert.
status optionalquerystring: pending | delivered | failed
Cursor optionalquerystringnextCursor from the previous page. Keep the same filters.
limit optionalqueryintegerPage size.

Responses

  • 200 A page of deliveries.
    FieldTypeAbout
    dataarray of WebhookDelivery
    nextCursorstring or null
    {
      "data": [
        {
          "id": "whd_88",
          "alertId": "alr_7",
          "event": "reviews.new",
          "status": "pending",
          "test": false,
          "attempts": 2,
          "responseStatus": 503,
          "error": "Webhook answered 503",
          "replayOf": null,
          "createdAt": "2026-10-28T09:30:00.000Z",
          "lastAttemptAt": "2026-10-28T09:31:00.000Z",
          "nextAttemptAt": "2026-10-28T10:01:00.000Z",
          "deliveredAt": null
        }
      ],
      "nextCursor": null
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 429 Too Many Requests

GET /api/v1/webhook-deliveries/{id}

Get a webhook delivery getWebhookDelivery

One delivery, with the exact body sent as payload.

Any key (read or write).

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 200 The delivery.
    FieldTypeAbout
    idstring optionalDelivery id.
    alertIdstring optionalThe webhook alert it went to.
    eventstring: reviews.new | profile.imported optionalThe event.
    statusstring: pending | delivered | failed optionalpending: will retry at nextAttemptAt.
    testboolean optionalA test send (POST /alerts/{id}/test); never retried.
    attemptsinteger optionalTries so far.
    responseStatusinteger or null optionalYour endpoint’s last HTTP status.
    errorstring or null optionalWhy the last try failed.
    replayOfstring or null optionalThe delivery this replays.
    createdAtstring optional
    lastAttemptAtstring or null optional
    nextAttemptAtstring or null optionalThe next retry, while pending.
    deliveredAtstring or null optional
    payloadobject optional
    {
      "id": "whd_88",
      "alertId": "alr_7",
      "event": "reviews.new",
      "status": "pending",
      "test": false,
      "attempts": 2,
      "responseStatus": 503,
      "error": "Webhook answered 503",
      "replayOf": null,
      "createdAt": "2026-10-28T09:30:00.000Z",
      "lastAttemptAt": "2026-10-28T09:31:00.000Z",
      "nextAttemptAt": "2026-10-28T10:01:00.000Z",
      "deliveredAt": null,
      "payload": {
        "id": "evt_9QwLx2",
        "event": "profile.imported",
        "livemode": true,
        "createdAt": "2026-10-28T09:30:00.000Z",
        "workspace": {
          "id": 1,
          "name": "Harbour & Vine"
        },
        "profile": {
          "id": 12,
          "locationId": 3,
          "locationName": "Harbour & Vine, Falmouth",
          "site": "tripadvisor",
          "name": "Tripadvisor: Harbour & Vine",
          "url": "https://www.tripadvisor.co.uk/Restaurant_Review-g186233-d1234567-Reviews-Harbour_Vine-Falmouth.html",
          "reviewCount": 214,
          "averageRating": 4.6,
          "lastCheckedAt": "2026-10-28T09:30:00.000Z",
          "nextCheckAt": "2026-10-28T10:30:00.000Z",
          "lastError": null,
          "createdAt": "2026-10-01T12:00:00.000Z"
        }
      }
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 404 Not Found
  • 429 Too Many Requests

POST /api/v1/webhook-deliveries/{id}/replay

Replay a webhook delivery replayWebhookDelivery

Sends the same body (same event id) to the same alert again, as a new delivery, signed afresh. It retries like any other.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Responses

  • 201 The new delivery.
    FieldTypeAbout
    idstringDelivery id.
    alertIdstringThe webhook alert it went to.
    eventstring: reviews.new | profile.importedThe event.
    statusstring: pending | delivered | failedpending: will retry at nextAttemptAt.
    testbooleanA test send (POST /alerts/{id}/test); never retried.
    attemptsintegerTries so far.
    responseStatusinteger or nullYour endpoint’s last HTTP status.
    errorstring or nullWhy the last try failed.
    replayOfstring or nullThe delivery this replays.
    createdAtstring
    lastAttemptAtstring or null
    nextAttemptAtstring or nullThe next retry, while pending.
    deliveredAtstring or null
    {
      "id": "whd_91",
      "alertId": "alr_7",
      "event": "reviews.new",
      "status": "delivered",
      "test": false,
      "attempts": 1,
      "responseStatus": 200,
      "error": null,
      "replayOf": "whd_88",
      "createdAt": "2026-10-28T09:30:00.000Z",
      "lastAttemptAt": "2026-10-28T09:31:00.000Z",
      "nextAttemptAt": null,
      "deliveredAt": "2026-10-28T10:05:00.000Z"
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests

Short links and QR codes to get reviews.

review requests

Ask customers for reviews by email or text, with a reminder and opt-outs.

POST /api/v1/locations/{id}/review-requests

Ask a customer for a review createReviewRequest

Queues an email (or text) to one customer with the location’s review link, sent within a minute, and one reminder later to people who haven’t clicked (see the settings). Needs the location’s review link and the sending rules agreed (409 otherwise). A customer asked at this location in the last 30 days, or who opted out, is refused with 409. Over the month’s allowance is 402, and so is any text: texts aren’t on a plan yet. Only ask real, recent customers who were told they’d be asked: transactedAt older than 180 days is refused.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Body

FieldTypeAbout
namestring optional
emailstring optional
phonestring optionalA mobile, +44 7… or 07….
channelstring: email | sms optionalDefaults to email when there’s one.
orderRefstring optional
transactedAtstring optionalWhen they bought or visited.
sendAtstring optionalSend later, up to 30 days ahead.
{
  "name": "Ann Price",
  "email": "ann@example.com",
  "orderRef": "A1043",
  "transactedAt": "2026-10-27"
}

Responses

  • 201 The request, queued.
    FieldTypeAbout
    idinteger
    locationIdstringThe location.
    locationNamestring or null
    channelstring: email | sms
    contactstringEmail address, or mobile number in E.164.
    namestring or null
    orderRefstring or null
    sourcestring: api | csv | webhook | app
    statusstring: queued | sent | failed | suppressedsuppressed: the customer opted out before it went.
    sendAtstring
    sentAtstring or null
    remindedAtstring or nullWhen the one reminder went, to people who hadn’t clicked.
    clickedAtstring or nullFirst click on the review link.
    reviewIdstring or nullThe review Earful matched to this request by the reviewer’s name.
    unsubscribedAtstring or null
    errorstring or null
    transactedAtstring or null
    createdAtstring
    {
      "id": 301,
      "locationId": "loc_4",
      "locationName": "Harbour & Vine, Falmouth",
      "channel": "email",
      "contact": "ann@example.com",
      "name": "Ann Price",
      "orderRef": "A1043",
      "source": "api",
      "status": "queued",
      "sendAt": "2026-10-28T09:30:00.000Z",
      "sentAt": null,
      "remindedAt": null,
      "clickedAt": null,
      "reviewId": null,
      "unsubscribedAt": null,
      "error": null,
      "transactedAt": "2026-10-27T19:00:00.000Z",
      "createdAt": "2026-10-28T09:30:00.000Z"
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 402 Payment Required
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests
  • 503 Service Unavailable

POST /api/v1/locations/{id}/review-requests/import

Ask customers from a CSV importReviewRequests

A CSV file’s text: a header row (name, email, phone, order_ref, transacted_at; common alternatives like first_name, last_name and mobile work too), then one customer a row, up to 2,000. Good rows are queued; the rest are listed with the reason.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Body

FieldTypeAbout
csvstring
{
  "csv": "name,email,order_ref\nAnn Price,ann@example.com,A1043\nSam Ellis,sam@example.com,A1044\n"
}

Responses

  • 200 What was queued and skipped.
    FieldTypeAbout
    createdinteger
    idsarray of integer
    skippedarray of object
    {
      "created": 2,
      "ids": [
        302,
        303
      ],
      "skipped": [
        {
          "row": 4,
          "reason": "That email address doesn’t look right."
        }
      ]
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 402 Payment Required
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests
  • 503 Service Unavailable

GET /api/v1/review-requests

List review requests listReviewRequests

Newest first.

Any key (read or write).

Parameters

NameInTypeAbout
location optionalquerystring
status optionalquerystring: queued | sent | failed | suppressed
Limit optionalqueryintegerPage size.
Offset optionalqueryintegerDeprecated: page with cursor. Rows to skip.
Cursor optionalquerystringnextCursor from the previous page. Keep the same filters.

Responses

  • 200 A page of requests.
    FieldTypeAbout
    dataarray of ReviewRequest
    limitinteger
    offsetinteger
    nextCursorstring or nullThe next page’s cursor; null after the last request.
    {
      "data": [
        {
          "id": 301,
          "locationId": "loc_4",
          "locationName": "Harbour & Vine, Falmouth",
          "channel": "email",
          "contact": "ann@example.com",
          "name": "Ann Price",
          "orderRef": "A1043",
          "source": "api",
          "status": "sent",
          "sendAt": "2026-10-28T09:30:00.000Z",
          "sentAt": "2026-10-28T09:30:00.000Z",
          "remindedAt": null,
          "clickedAt": "2026-10-28T09:30:00.000Z",
          "reviewId": "rev_5512",
          "unsubscribedAt": null,
          "error": null,
          "transactedAt": "2026-10-27T19:00:00.000Z",
          "createdAt": "2026-10-28T09:30:00.000Z"
        }
      ],
      "limit": 50,
      "offset": 0,
      "nextCursor": null
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 429 Too Many Requests

GET /api/v1/review-requests/{id}

Get a review request getReviewRequest

One request and how far it got.

Any key (read or write).

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 200 The request.
    FieldTypeAbout
    idinteger
    locationIdstringThe location.
    locationNamestring or null
    channelstring: email | sms
    contactstringEmail address, or mobile number in E.164.
    namestring or null
    orderRefstring or null
    sourcestring: api | csv | webhook | app
    statusstring: queued | sent | failed | suppressedsuppressed: the customer opted out before it went.
    sendAtstring
    sentAtstring or null
    remindedAtstring or nullWhen the one reminder went, to people who hadn’t clicked.
    clickedAtstring or nullFirst click on the review link.
    reviewIdstring or nullThe review Earful matched to this request by the reviewer’s name.
    unsubscribedAtstring or null
    errorstring or null
    transactedAtstring or null
    createdAtstring
    {
      "id": 301,
      "locationId": "loc_4",
      "locationName": "Harbour & Vine, Falmouth",
      "channel": "email",
      "contact": "ann@example.com",
      "name": "Ann Price",
      "orderRef": "A1043",
      "source": "api",
      "status": "queued",
      "sendAt": "2026-10-28T09:30:00.000Z",
      "sentAt": null,
      "remindedAt": null,
      "clickedAt": null,
      "reviewId": null,
      "unsubscribedAt": null,
      "error": null,
      "transactedAt": "2026-10-27T19:00:00.000Z",
      "createdAt": "2026-10-28T09:30:00.000Z"
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 404 Not Found
  • 429 Too Many Requests

GET /api/v1/review-requests/stats

Review request funnel getReviewRequestStats

For requests made in the last days (7, 30, 90 or 365; 30 by default): how many were sent, clicked, matched to a review (by the reviewer’s name, at the same location, within 30 days), reminded and opted out. Plus this month’s sends against the plan.

Any key (read or write).

Parameters

NameInTypeAbout
location optionalqueryinteger
days optionalqueryinteger: 7 | 30 | 90 | 365

Responses

  • 200 The funnel.
    FieldTypeAbout
    daysinteger
    requestedinteger
    queuedinteger
    sentinteger
    failedinteger
    suppressedinteger
    remindedinteger
    clickedinteger
    reviewedinteger
    unsubscribedinteger
    clickRatenumber or nullclicked / sent.
    reviewRatenumber or nullreviewed / sent.
    usageobject
    emailsinteger
    emailLimitinteger
    textsinteger
    textLimitinteger
    {
      "days": 30,
      "requested": 240,
      "queued": 4,
      "sent": 230,
      "failed": 2,
      "suppressed": 4,
      "reminded": 120,
      "clicked": 88,
      "reviewed": 41,
      "unsubscribed": 3,
      "clickRate": 0.383,
      "reviewRate": 0.178,
      "usage": {
        "emails": 212,
        "emailLimit": 1100,
        "texts": 31,
        "textLimit": 250
      }
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 404 Not Found
  • 429 Too Many Requests

GET /api/v1/review-requests/settings

Get message settings getReviewRequestSettings

The email, reminder and text templates, the reminder delay, where replies go, whether the sending rules are agreed, and suggested consent wording.

Any key (read or write).

Responses

  • 200 The settings.
    FieldTypeAbout
    emailSubjectstring
    emailBodystringMust contain {link}. {name} and {business} are filled in.
    reminderSubjectstring
    reminderBodystring
    smsBodystring
    reminderDaysinteger0 for no reminder.
    replyTostring or null
    acknowledgedbooleanWhether the owner agreed the sending rules; requests need it.
    acknowledgedAtstring or null
    consentWordingstringSuggested wording for receipts and booking forms.
    {
      "emailSubject": "How was your visit to {business}?",
      "emailBody": "Hi {name},\n\nWould you take a minute to leave us a review?\n\n{link}\n\nThank you,\n{business}",
      "reminderSubject": "A quick reminder from {business}",
      "reminderBody": "Hi {name}, here’s the link again: {link}",
      "smsBody": "Hi {name}, thanks for choosing {business}. Would you leave us a quick review? {link}",
      "reminderDays": 3,
      "replyTo": "hello@harbourandvine.co.uk",
      "acknowledged": true,
      "acknowledgedAt": "2026-10-28T09:30:00.000Z",
      "consentWording": "We’ll send you one email (or text) after your visit asking for a review, and one reminder. You can opt out now, or with the link in the message."
    }
  • 401 Unauthorized
  • 429 Too Many Requests

PUT /api/v1/review-requests/settings

Change message settings setReviewRequestSettings

Fields left out keep their value. Templates must contain {link}; Earful adds the opt-out line. Send acknowledged: true once to agree the sending rules: only real customers who bought or booked recently, who were told they’d be asked and could say no.

Needs a write key.

Body

FieldTypeAbout
emailSubjectstring optional
emailBodystring optional
reminderSubjectstring optional
reminderBodystring optional
smsBodystring optional
reminderDaysinteger: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 14 optional
replyTostring or null optional
acknowledgedboolean optional
{
  "acknowledged": true
}

Responses

  • 200 The settings.
    FieldTypeAbout
    emailSubjectstring
    emailBodystringMust contain {link}. {name} and {business} are filled in.
    reminderSubjectstring
    reminderBodystring
    smsBodystring
    reminderDaysinteger0 for no reminder.
    replyTostring or null
    acknowledgedbooleanWhether the owner agreed the sending rules; requests need it.
    acknowledgedAtstring or null
    consentWordingstringSuggested wording for receipts and booking forms.
    {
      "emailSubject": "How was your visit to {business}?",
      "emailBody": "Hi {name},\n\nWould you take a minute to leave us a review?\n\n{link}\n\nThank you,\n{business}",
      "reminderSubject": "A quick reminder from {business}",
      "reminderBody": "Hi {name}, here’s the link again: {link}",
      "smsBody": "Hi {name}, thanks for choosing {business}. Would you leave us a quick review? {link}",
      "reminderDays": 3,
      "replyTo": "hello@harbourandvine.co.uk",
      "acknowledged": true,
      "acknowledgedAt": "2026-10-28T09:30:00.000Z",
      "consentWording": "We’ll send you one email (or text) after your visit asking for a review, and one reminder. You can opt out now, or with the link in the message."
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 429 Too Many Requests

GET /api/v1/suppressions

List opt-outs listSuppressions

Customers who opted out of review requests, or were added by hand. They’re never asked again.

Any key (read or write).

Responses

  • 200 Opt-outs, newest first.
    FieldTypeAbout
    dataarray of Suppression
    {
      "data": [
        {
          "id": 7,
          "contact": "sam@example.com",
          "reason": "unsubscribed",
          "createdAt": "2026-10-28T09:30:00.000Z"
        }
      ]
    }
  • 401 Unauthorized
  • 429 Too Many Requests

POST /api/v1/suppressions

Add an opt-out addSuppression

An email address or mobile number never to ask.

Needs a write key.

Parameters

NameInTypeAbout
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Body

FieldTypeAbout
contactstring
{
  "contact": "sam@example.com"
}

Responses

  • 201 The opt-out.
    FieldTypeAbout
    idinteger
    contactstring
    reasonstring: unsubscribed | manual
    createdAtstring
    {
      "id": 8,
      "contact": "+447700900123",
      "reason": "manual",
      "createdAt": "2026-10-28T09:30:00.000Z"
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests

DELETE /api/v1/suppressions/{id}

Remove an opt-out deleteSuppression

Only when the customer asks to hear from you again.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 204 Done; no body.
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 429 Too Many Requests

GET /api/v1/locations/{id}/request-hook

Get a location’s webhook URL getRequestHook

The secret URL a till, booking system or Zapier posts customers to. 404 until one is made.

Any key (read or write).

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 200 The URL.
    FieldTypeAbout
    locationIdstringThe location.
    urlstring
    {
      "locationId": "loc_4",
      "url": "https://earful.onibaku.co.uk/api/v1/hooks/review-requests/k3P9xQ2mRt7wZb4nVh8c"
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 404 Not Found
  • 429 Too Many Requests

POST /api/v1/locations/{id}/request-hook

Make a new webhook URL rotateRequestHook

Makes the location’s webhook URL, or replaces it (the old one stops working at once).

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Responses

  • 201 The URL.
    FieldTypeAbout
    locationIdstringThe location.
    urlstring
    {
      "locationId": "loc_4",
      "url": "https://earful.onibaku.co.uk/api/v1/hooks/review-requests/k3P9xQ2mRt7wZb4nVh8c"
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests

DELETE /api/v1/locations/{id}/request-hook

Remove the webhook URL deleteRequestHook

The URL stops working.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 204 Done; no body.
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 429 Too Many Requests

POST /api/v1/hooks/review-requests/{code}

Webhook: ask a customer for a review postRequestHook

No key: the secret URL from the location’s webhook is the credential. Send one customer as JSON or a form, with field names as your system has them (name or first_name and last_name; email; phone or mobile; order_ref, order_id or booking_id; transacted_at). In Zapier, use Webhooks by Zapier, POST. A repeat or opted-out customer answers 202 with status skipped, so a till can post every sale.

Parameters

NameInTypeAbout
codepathstringFrom the webhook URL.

Body

object

{
  "first_name": "Ann",
  "last_name": "Price",
  "email": "ann@example.com",
  "order_id": "A1043"
}

Responses

  • 202 Queued, or skipped with the reason.
    FieldTypeAbout
    idinteger or null
    statusstring: queued | skipped
    reasonstring or null
    {
      "id": 304,
      "status": "queued",
      "reason": null
    }
  • 400 Bad Request
  • 402 Payment Required
  • 404 Not Found
  • 409 Conflict
  • 429 Too Many Requests
  • 503 Service Unavailable

apps

App Store and Google Play rating trends and AI topics.

GET /api/v1/apps

List apps listApps

App Store and Google Play profiles, with their rating and the newest version a review mentions. Their reviews are GET /api/v1/reviews?profile={id}, filterable by version and country.

Any key (read or write).

Responses

  • 200 Apps.
    FieldTypeAbout
    dataarray of App
    {
      "data": [
        {
          "id": "prf_12",
          "locationId": "loc_6",
          "site": "appstore",
          "name": "App Store: Harbour & Vine",
          "url": "https://apps.apple.com/gb/app/harbour-vine/id1234567890",
          "reviewCount": 412,
          "averageRating": 4.3,
          "latestVersion": "5.2.0"
        }
      ]
    }
  • 401 Unauthorized
  • 429 Too Many Requests

GET /api/v1/apps/{id}/trends

App rating trends getAppTrends

Average rating by week, by app version (with when each version’s first review arrived, which marks the release on a chart) and by store country, over the last 30, 90 (default) or 365 days.

Any key (read or write).

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.
days optionalqueryinteger: 30 | 90 | 365

Responses

  • 200 Trends.
    FieldTypeAbout
    appApp
    daysinteger
    weeksarray of object
    versionsarray of object
    countriesarray of object
    {
      "app": {
        "id": "prf_12",
        "locationId": "loc_6",
        "site": "appstore",
        "name": "App Store: Harbour & Vine",
        "url": "https://apps.apple.com/gb/app/harbour-vine/id1234567890",
        "reviewCount": 412,
        "averageRating": 4.3,
        "latestVersion": "5.2.0"
      },
      "days": 90,
      "weeks": [
        {
          "week": "2026-10-19",
          "reviews": 31,
          "averageRating": 3.9
        }
      ],
      "versions": [
        {
          "version": "5.2.0",
          "firstSeenAt": "2026-10-20T08:12:00.000Z",
          "reviews": 28,
          "averageRating": 3.7
        }
      ],
      "countries": [
        {
          "country": "GB",
          "reviews": 25,
          "averageRating": 4.1
        }
      ]
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 404 Not Found
  • 429 Too Many Requests

GET /api/v1/apps/{id}/topics

Get app topics getAppTopics

The latest AI grouping of the app’s reviews into topics. 404 until it’s been run.

Any key (read or write).

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 200 Topics.
    FieldTypeAbout
    appIdstringThe app’s profile id.
    reviewCountintegerReviews the topics were drawn from (the newest written ones).
    topicsarray of object
    createdAtstring
    {
      "appId": "prf_12",
      "reviewCount": 200,
      "topics": [
        {
          "title": "Crashes when uploading a photo",
          "kind": "bug",
          "summary": "Since 5.2.0 the app closes when attaching a photo.",
          "reviewIds": [
            5512,
            5520
          ]
        },
        {
          "title": "Wants a dark mode",
          "kind": "feature_request",
          "summary": "People reading at night ask for a dark theme.",
          "reviewIds": [
            5490
          ]
        }
      ],
      "createdAt": "2026-10-28T09:30:00.000Z"
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 404 Not Found
  • 429 Too Many Requests

POST /api/v1/apps/{id}/topics

Find app topics findAppTopics

Groups the app’s 200 newest written reviews into up to 12 topics (bugs, feature requests, complaints, praise, questions) with AI. Plus plan; 10 runs an hour per workspace; 503 when AI is off on the server.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Responses

  • 201 Topics.
    FieldTypeAbout
    appIdstringThe app’s profile id.
    reviewCountintegerReviews the topics were drawn from (the newest written ones).
    topicsarray of object
    createdAtstring
    {
      "appId": "prf_12",
      "reviewCount": 200,
      "topics": [
        {
          "title": "Crashes when uploading a photo",
          "kind": "bug",
          "summary": "Since 5.2.0 the app closes when attaching a photo.",
          "reviewIds": [
            5512,
            5520
          ]
        },
        {
          "title": "Wants a dark mode",
          "kind": "feature_request",
          "summary": "People reading at night ask for a dark theme.",
          "reviewIds": [
            5490
          ]
        }
      ],
      "createdAt": "2026-10-28T09:30:00.000Z"
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 402 Payment Required
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests
  • 503 Service Unavailable

map rankings

Google Maps rank on a grid of points around a location.

GET /api/v1/rank-trackers

List rank trackers listRankTrackers

Each tracker with its latest scan.

Any key (read or write).

Responses

  • 200 Trackers.
    FieldTypeAbout
    dataarray of RankTracker
    {
      "data": [
        {
          "id": 9,
          "locationId": "loc_4",
          "keyword": "seafood restaurant",
          "gridSize": 7,
          "radiusKm": 2,
          "center": {
            "lat": 50.153,
            "lng": -5.067
          },
          "business": {
            "cid": "1234567890123456789",
            "name": "Harbour & Vine"
          },
          "every": "week",
          "nextScanAt": "2026-11-04T09:30:00.000Z",
          "createdAt": "2026-10-28T09:30:00.000Z",
          "scans": [
            {
              "id": 88,
              "trackerId": 9,
              "status": "done",
              "points": 49,
              "averageRank": 3.4,
              "top3Share": 0.592,
              "foundShare": 0.918,
              "costUsd": 0.0294,
              "createdAt": "2026-10-28T09:30:00.000Z",
              "finishedAt": "2026-10-28T09:30:00.000Z"
            }
          ]
        }
      ]
    }
  • 401 Unauthorized
  • 429 Too Many Requests

POST /api/v1/locations/{id}/rank-trackers

Track a Google Maps search createRankTracker

Where the location ranks on Google Maps for keyword, at each point of a gridSize × gridSize grid (3 to 13, odd; 7 by default) radiusKm (0.5 to 25; 2 by default) from the centre to each edge. Scans straight away; every rescans on a schedule. The centre and the business (its cid, else its name) come from the location’s Google Maps page, or send lat, lng and name. Each point uses one of the plan’s monthly grid points (402 when they run out); 503 when DataForSEO isn’t set up on the server.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Body

FieldTypeAbout
keywordstring
gridSizeinteger: 3 | 5 | 7 | 9 | 11 | 13 optional
radiusKmnumber optional
everystring: week | fortnight | month optional
latnumber optional
lngnumber optional
namestring optionalThe business name exactly as Google Maps shows it.
cidstring optionalGoogle’s customer id for the business.
{
  "keyword": "seafood restaurant",
  "gridSize": 7,
  "radiusKm": 2,
  "every": "week"
}

Responses

  • 201 The tracker, with its first scan running.
    FieldTypeAbout
    idinteger
    locationIdstringThe location.
    keywordstring
    gridSizeinteger: 3 | 5 | 7 | 9 | 11 | 13
    radiusKmnumber
    centerobject
    latnumber
    lngnumber
    businessobject
    cidstring or null
    namestring
    everystring or null: week | fortnight | month
    nextScanAtstring or null
    createdAtstring
    scansarray of RankScanNewest first.
    {
      "id": 9,
      "locationId": "loc_4",
      "keyword": "seafood restaurant",
      "gridSize": 7,
      "radiusKm": 2,
      "center": {
        "lat": 50.153,
        "lng": -5.067
      },
      "business": {
        "cid": "1234567890123456789",
        "name": "Harbour & Vine"
      },
      "every": "week",
      "nextScanAt": "2026-11-04T09:30:00.000Z",
      "createdAt": "2026-10-28T09:30:00.000Z",
      "scans": [
        {
          "id": 88,
          "trackerId": 9,
          "status": "running",
          "points": 49,
          "averageRank": null,
          "top3Share": null,
          "foundShare": null,
          "costUsd": null,
          "createdAt": "2026-10-28T09:30:00.000Z",
          "finishedAt": null
        }
      ]
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 402 Payment Required
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests
  • 503 Service Unavailable

GET /api/v1/rank-trackers/{id}

Get a rank tracker getRankTracker

The tracker and its scan history, newest first (up to a year of weekly scans).

Any key (read or write).

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 200 The tracker.
    FieldTypeAbout
    idinteger
    locationIdstringThe location.
    keywordstring
    gridSizeinteger: 3 | 5 | 7 | 9 | 11 | 13
    radiusKmnumber
    centerobject
    latnumber
    lngnumber
    businessobject
    cidstring or null
    namestring
    everystring or null: week | fortnight | month
    nextScanAtstring or null
    createdAtstring
    scansarray of RankScanNewest first.
    {
      "id": 9,
      "locationId": "loc_4",
      "keyword": "seafood restaurant",
      "gridSize": 7,
      "radiusKm": 2,
      "center": {
        "lat": 50.153,
        "lng": -5.067
      },
      "business": {
        "cid": "1234567890123456789",
        "name": "Harbour & Vine"
      },
      "every": "week",
      "nextScanAt": "2026-11-04T09:30:00.000Z",
      "createdAt": "2026-10-28T09:30:00.000Z",
      "scans": [
        {
          "id": 88,
          "trackerId": 9,
          "status": "done",
          "points": 49,
          "averageRank": 3.4,
          "top3Share": 0.592,
          "foundShare": 0.918,
          "costUsd": 0.0294,
          "createdAt": "2026-10-28T09:30:00.000Z",
          "finishedAt": "2026-10-28T09:30:00.000Z"
        }
      ]
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 404 Not Found
  • 429 Too Many Requests

DELETE /api/v1/rank-trackers/{id}

Delete a rank tracker deleteRankTracker

Deletes it with its scans.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 204 Done; no body.
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 429 Too Many Requests

POST /api/v1/rank-trackers/{id}/scans

Scan again scanRankTracker

Starts a scan now (409 while one is running). Points finish within minutes; poll GET /api/v1/rank-scans/{id}.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Responses

  • 202 The scan, running.
    FieldTypeAbout
    idinteger
    trackerIdinteger
    statusstring: running | done | failed
    pointsintegerGrid points, which count against the plan’s monthly allowance.
    averageRanknumber or nullAverage Maps rank over the points where the business shows.
    top3Sharenumber or nullShare of points (0–1) where it’s in the top 3.
    foundSharenumber or nullShare of points (0–1) where it’s in the top 20.
    costUsdnumber or null
    createdAtstring
    finishedAtstring or null
    {
      "id": 88,
      "trackerId": 9,
      "status": "running",
      "points": 49,
      "averageRank": null,
      "top3Share": null,
      "foundShare": null,
      "costUsd": null,
      "createdAt": "2026-10-28T09:30:00.000Z",
      "finishedAt": null
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 402 Payment Required
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests
  • 503 Service Unavailable

GET /api/v1/rank-scans/{id}

Get a rank scan getRankScan

The rank at every grid point (row by row from the north-west), the top 3 there, and the businesses that most often beat this one.

Any key (read or write).

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 200 The scan.
    FieldTypeAbout
    idinteger optional
    trackerIdinteger optional
    statusstring: running | done | failed optional
    pointsinteger optionalGrid points, which count against the plan’s monthly allowance.
    averageRanknumber or null optionalAverage Maps rank over the points where the business shows.
    top3Sharenumber or null optionalShare of points (0–1) where it’s in the top 3.
    foundSharenumber or null optionalShare of points (0–1) where it’s in the top 20.
    costUsdnumber or null optional
    createdAtstring optional
    finishedAtstring or null optional
    competitorsarray of object optional
    gridarray of object optionalRow by row from the north-west corner.
    {
      "id": 88,
      "trackerId": 9,
      "status": "done",
      "points": 49,
      "averageRank": 3.4,
      "top3Share": 0.592,
      "foundShare": 0.918,
      "costUsd": 0.0294,
      "createdAt": "2026-10-28T09:30:00.000Z",
      "finishedAt": "2026-10-28T09:30:00.000Z",
      "competitors": [
        {
          "name": "The Shack",
          "cid": "987654321",
          "appearances": 31,
          "averageRank": 1.6
        }
      ],
      "grid": [
        {
          "index": 0,
          "lat": 50.17099,
          "lng": -5.09509,
          "status": "done",
          "rank": 4,
          "top": [
            {
              "rank": 1,
              "title": "The Shack",
              "cid": "987654321"
            }
          ]
        }
      ]
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 404 Not Found
  • 429 Too Many Requests

workspace

Reply voice, plan and usage.

GET /api/v1/workspace

Workspace, plan and usage getWorkspace

The key’s workspace: its reply voice, plan limits and what’s used of them this month.

Any key (read or write).

Responses

  • 200 The workspace.
    FieldTypeAbout
    idintegerWorkspace id.
    namestringWorkspace name.
    businessNamestring or nullThe name AI drafts sign as.
    tonestring: friendly | professional | playful | concise
    voiceNotesstring or null
    signoffstring or null
    planobject
    namestring: free | pro
    locationsintegerLocations the plan covers.
    checkEveryMinutesintegerHow often reviews are collected.
    aiDraftsPerMonthinteger or nullnull for unlimited.
    alertChannelsarray of string
    directRepliesbooleanWhether replies post straight to sites with an owner API.
    addonsobjectHow many of the plan’s locations the Local add-on (map rank points) covers.
    requestEmailsPerMonthinteger
    requestTextsPerMonthinteger
    rankPointsPerMonthintegerMap rank grid points a month.
    appTopicsbooleanWhether AI topics for apps are included.
    usageobject
    locationsintegerLocations in use.
    profilesintegerReview pages in use.
    aiDraftsThisMonthintegerAI drafts made this calendar month.
    aiDraftsLeftinteger or nullnull for unlimited.
    requestEmailsThisMonthintegerSent and queued this calendar month.
    requestTextsThisMonthinteger
    rankPointsThisMonthinteger
    autoReplyobjectAuto-replies: see reply rules.
    enabledbooleanOff until you turn it on.
    dailyCapintegerMost replies posted automatically in 24 hours.
    {
      "id": 1,
      "name": "Harbour & Vine reviews",
      "businessName": "Harbour & Vine",
      "tone": "friendly",
      "voiceNotes": "Family-run. Use first names.",
      "signoff": "Sam and the team",
      "plan": {
        "name": "pro",
        "locations": 3,
        "checkEveryMinutes": 60,
        "aiDraftsPerMonth": null,
        "alertChannels": [
          "digest",
          "email",
          "slack",
          "webhook"
        ],
        "directReplies": true,
        "addons": {
          "local": 0
        },
        "requestEmailsPerMonth": 3000,
        "requestTextsPerMonth": 0,
        "rankPointsPerMonth": 450,
        "appTopics": true
      },
      "usage": {
        "locations": 2,
        "profiles": 5,
        "aiDraftsThisMonth": 41,
        "aiDraftsLeft": null,
        "requestEmailsThisMonth": 212,
        "requestTextsThisMonth": 0,
        "rankPointsThisMonth": 98
      },
      "autoReply": {
        "enabled": false,
        "dailyCap": 20
      }
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 429 Too Many Requests

PATCH /api/v1/workspace

Update the reply voice updateWorkspace

Changes the workspace name and what AI drafts are written from. Fields left out keep their value; send null to clear one.

Needs a write key.

Body

FieldTypeAbout
namestring optional
businessNamestring or null optional
tonestring: friendly | professional | playful | concise optional
voiceNotesstring or null optional
signoffstring or null optional
autoReplyobject optional
enabledboolean optionalOff until you turn it on.
dailyCapinteger optionalMost replies posted automatically in 24 hours.
{
  "tone": "playful",
  "signoff": "Sam and the team"
}

Responses

  • 200 The updated workspace.
    FieldTypeAbout
    idintegerWorkspace id.
    namestringWorkspace name.
    businessNamestring or nullThe name AI drafts sign as.
    tonestring: friendly | professional | playful | concise
    voiceNotesstring or null
    signoffstring or null
    planobject
    namestring: free | pro
    locationsintegerLocations the plan covers.
    checkEveryMinutesintegerHow often reviews are collected.
    aiDraftsPerMonthinteger or nullnull for unlimited.
    alertChannelsarray of string
    directRepliesbooleanWhether replies post straight to sites with an owner API.
    addonsobjectHow many of the plan’s locations the Local add-on (map rank points) covers.
    requestEmailsPerMonthinteger
    requestTextsPerMonthinteger
    rankPointsPerMonthintegerMap rank grid points a month.
    appTopicsbooleanWhether AI topics for apps are included.
    usageobject
    locationsintegerLocations in use.
    profilesintegerReview pages in use.
    aiDraftsThisMonthintegerAI drafts made this calendar month.
    aiDraftsLeftinteger or nullnull for unlimited.
    requestEmailsThisMonthintegerSent and queued this calendar month.
    requestTextsThisMonthinteger
    rankPointsThisMonthinteger
    autoReplyobjectAuto-replies: see reply rules.
    enabledbooleanOff until you turn it on.
    dailyCapintegerMost replies posted automatically in 24 hours.
    {
      "id": 1,
      "name": "Harbour & Vine reviews",
      "businessName": "Harbour & Vine",
      "tone": "friendly",
      "voiceNotes": "Family-run. Use first names.",
      "signoff": "Sam and the team",
      "plan": {
        "name": "pro",
        "locations": 3,
        "checkEveryMinutes": 60,
        "aiDraftsPerMonth": null,
        "alertChannels": [
          "digest",
          "email",
          "slack",
          "webhook"
        ],
        "directReplies": true,
        "addons": {
          "local": 0
        },
        "requestEmailsPerMonth": 3000,
        "requestTextsPerMonth": 0,
        "rankPointsPerMonth": 450,
        "appTopics": true
      },
      "usage": {
        "locations": 2,
        "profiles": 5,
        "aiDraftsThisMonth": 41,
        "aiDraftsLeft": null,
        "requestEmailsThisMonth": 212,
        "requestTextsThisMonth": 0,
        "rankPointsThisMonth": 98
      },
      "autoReply": {
        "enabled": false,
        "dailyCap": 20
      }
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 429 Too Many Requests

api keys

Create, rotate and revoke keys.

GET /api/v1/keys

List API keys listKeys

Every REST API key in the workspace, with when, where from and how often each was used. Keys themselves are never shown again.

Any key (read or write).

Responses

  • 200 The keys.
    FieldTypeAbout
    dataarray of ApiKey
    {
      "data": [
        {
          "id": 4,
          "name": "BI export",
          "scope": "read",
          "prefix": "ef_3k9Qw1x",
          "createdAt": "2026-10-28T09:30:00.000Z",
          "expiresAt": null,
          "lastUsedAt": "2026-10-28T09:30:00.000Z",
          "lastUsedIp": "203.0.113.7",
          "requestCount": 1288,
          "mode": "live"
        }
      ]
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 429 Too Many Requests

POST /api/v1/keys

Create a key createKey

Makes a key. The answer holds the key itself, once; Earful keeps only its hash.

Needs a write key.

Parameters

NameInTypeAbout
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Body

FieldTypeAbout
namestring optional
scopestring: read | write optional
expiresInDaysinteger optionalLeave out for a key that doesn’t expire.
modestring: live | test optionaltest makes an ef_test_ key for the workspace’s sandbox: made-up data, no outside calls.
{
  "name": "BI export",
  "scope": "read",
  "expiresInDays": 90
}

Responses

  • 201 The key, shown once.
    FieldTypeAbout
    idinteger optionalKey id.
    namestring optionalWhat it’s for.
    scopestring: read | write optionalread: GET only. write: everything.
    prefixstring optionalThe start of the key, to tell keys apart.
    createdAtstring optionalWhen it was made.
    expiresAtstring or null optionalWhen it stops working; null for never. A rotated key’s grace period ends here.
    lastUsedAtstring or null optionalWhen it was last used.
    lastUsedIpstring or null optionalThe address it was last used from.
    requestCountinteger optionalRequests made with it.
    modestring: live | test optionaltest: an ef_test_ key, which reaches the workspace’s sandbox (made-up data, no outside calls).
    keystring optionalThe key itself. Shown only in this answer; Earful keeps a hash.
    {
      "id": 4,
      "name": "BI export",
      "scope": "read",
      "prefix": "ef_3k9Qw1x",
      "createdAt": "2026-10-28T09:30:00.000Z",
      "expiresAt": null,
      "lastUsedAt": null,
      "lastUsedIp": null,
      "requestCount": 0,
      "key": "ef_3k9Qw1x…",
      "mode": "live"
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests

PATCH /api/v1/keys/{id}

Rename a key renameKey

Changes a key’s name.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Body

FieldTypeAbout
namestring
{
  "name": "Looker"
}

Responses

  • 200 The key.
    FieldTypeAbout
    idintegerKey id.
    namestringWhat it’s for.
    scopestring: read | writeread: GET only. write: everything.
    prefixstringThe start of the key, to tell keys apart.
    createdAtstringWhen it was made.
    expiresAtstring or nullWhen it stops working; null for never. A rotated key’s grace period ends here.
    lastUsedAtstring or nullWhen it was last used.
    lastUsedIpstring or nullThe address it was last used from.
    requestCountintegerRequests made with it.
    modestring: live | testtest: an ef_test_ key, which reaches the workspace’s sandbox (made-up data, no outside calls).
    {
      "id": 4,
      "name": "Looker",
      "scope": "read",
      "prefix": "ef_3k9Qw1x",
      "createdAt": "2026-10-28T09:30:00.000Z",
      "expiresAt": null,
      "lastUsedAt": "2026-10-28T09:30:00.000Z",
      "lastUsedIp": "203.0.113.7",
      "requestCount": 1288,
      "mode": "live"
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 429 Too Many Requests

DELETE /api/v1/keys/{id}

Revoke a key revokeKey

Stops a key working, straight away.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 204 Done; no body.
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 429 Too Many Requests

POST /api/v1/keys/{id}/rotate

Rotate a key rotateKey

Makes a new key with the same name, scope and expiry, and ends the old one after a grace period (24 hours unless you say; 0 ends it now) so callers can switch without downtime.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.
IdempotencyKey optionalheaderstringAny unique string, like a UUID. Retrying with the same key within 24 hours gets the first answer back (with Idempotent-Replayed: true) instead of doing it twice. The same key on a different request is a 422.

Body

FieldTypeAbout
graceHoursinteger optional
{
  "graceHours": 168
}

Responses

  • 201 The new key, shown once, and the old one’s new expiry.
    FieldTypeAbout
    previousApiKey optional
    {
      "id": 5,
      "name": "BI export",
      "scope": "read",
      "prefix": "ef_3k9Qw1x",
      "createdAt": "2026-10-28T09:30:00.000Z",
      "expiresAt": null,
      "lastUsedAt": "2026-10-28T09:30:00.000Z",
      "lastUsedIp": "203.0.113.7",
      "requestCount": 0,
      "key": "ef_Zp0…",
      "previous": {
        "id": 4,
        "name": "BI export",
        "scope": "read",
        "prefix": "ef_3k9Qw1x",
        "createdAt": "2026-10-28T09:30:00.000Z",
        "expiresAt": "2026-10-29T09:30:00.000Z",
        "lastUsedAt": "2026-10-28T09:30:00.000Z",
        "lastUsedIp": "203.0.113.7",
        "requestCount": 1288,
        "mode": "live"
      },
      "mode": "live"
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 422 Unprocessable Content
  • 429 Too Many Requests

connected apps

MCP clients signed in with OAuth, and disconnecting them.

GET /api/v1/connected-apps

List connected apps listConnectedApps

Apps connected to the workspace’s MCP server with OAuth (claude.ai connectors and the like), with who allowed them and when they were last used.

Any key (read or write).

Responses

  • 200 The connected apps.
    FieldTypeAbout
    dataarray of ConnectedApp
    {
      "data": [
        {
          "id": 2,
          "name": "Claude",
          "clientId": "efc_Zk3PqA9wX1bT0cV7nL2mRg",
          "redirectHosts": [
            "claude.ai"
          ],
          "scope": "write",
          "connectedBy": "owner@harbour.example",
          "createdAt": "2026-10-28T09:30:00.000Z",
          "lastUsedAt": "2026-10-28T10:02:00.000Z",
          "lastUsedIp": "203.0.113.7",
          "requestCount": 42
        }
      ]
    }
  • 400 Bad Request
  • 401 Unauthorized
  • 429 Too Many Requests

DELETE /api/v1/connected-apps/{id}

Disconnect an app revokeConnectedApp

Revokes the app’s access and refresh tokens straight away. It has to be connected again to come back.

Needs a write key.

Parameters

NameInTypeAbout
IdpathstringThe resource’s id, like loc_3. The bare number still works.

Responses

  • 204 Done; no body.
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 429 Too Many Requests

free tools

The no-sign-up tools on the Earful site. No key, a small daily allowance per IP address.

GET /api/v1/tools/google-places

Free tool: find a Google place ID and review link findGooglePlace

No key. Looks a business up on Google by name and answers each match with its place ID and write-a-review link. 10 new lookups a day per IP address; repeat lookups come from a cache and don’t count. With a key, use place searches instead.

Parameters

NameInTypeAbout
qquerystringThe business name.
near optionalquerystringA town or postcode to narrow it down.

Responses

  • 200 Matches, best first. Empty when nothing matched.
    FieldTypeAbout
    dataarray of object
    {
      "data": [
        {
          "name": "Salami Social Club",
          "address": "12 High Street, Leeds LS1 4AB",
          "placeId": "ChIJN1t_tDeuEmsRUsoyG83frY4",
          "reviewUrl": "https://search.google.com/local/writereview?placeid=ChIJN1t_tDeuEmsRUsoyG83frY4",
          "rating": 4.6,
          "reviewCount": 412
        }
      ]
    }
  • 400 Bad Request
  • 429 Too Many Requests
  • 503 Service Unavailable

POST /api/v1/tools/reply-drafts

Free tool: draft a reply to a pasted review draftFreeReply

No key. Drafts a reply to one review in the tone you pick. Nothing is stored. 5 drafts a day per IP address, reviews up to 2,000 characters. With a key, draft replies to collected reviews instead, in your workspace’s voice.

Body

FieldTypeAbout
reviewstring
ratinginteger or null optional
tonestring: friendly | professional | playful | concise optional
businessNamestring or null optional
{
  "review": "Waited 40 minutes for our mains and nobody said why.",
  "rating": 2,
  "tone": "professional",
  "businessName": "Salami Social Club"
}

Responses

  • 200 The draft.
    FieldTypeAbout
    replystring
    {
      "reply": "Thank you for telling us, and we’re sorry about the wait. That isn’t the evening we want anyone to have. Please get in touch so we can look into what happened."
    }
  • 400 Bad Request
  • 422 Unprocessable Content
  • 429 Too Many Requests
  • 502 Service Unavailable
  • 503 Service Unavailable

meta

This spec.

GET /api/v1/openapi.json

This specification getOpenApi

The OpenAPI 3.1 document for this API, with this server as its server URL. No key needed.

Responses

  • 200 The spec.

    object

    {
      "openapi": "3.1.0"
    }

webhooks

What Earful POSTs to a webhook alert’s URL.

POST reviews.new

New reviews reviewsNew

New reviews that match a webhook alert’s filters.

Parameters

NameInTypeAbout
Earful-Signatureheaderstringt=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body" with the alert’s secret>. Refuse it if t is over 5 minutes off.
Earful-Event optionalheaderstring
Earful-Delivery optionalheaderstringThe delivery’s number.

Body

FieldTypeAbout
idstringThe event’s id (evt_…); a replay sends the same one, so dedupe on it.
eventany
livemodebooleanfalse when it comes from the test-mode sandbox (an ef_test_ key’s workspace).
createdAtstring
workspaceobject
idinteger
namestring
reviewsarray of Review

Responses

  • 200 Any 2xx counts as delivered; anything else is retried.

POST profile.imported

Profile imported profileImported

A profile’s history import is over. Sent to webhook alerts whose site and location filters fit the profile.

Parameters

NameInTypeAbout
Earful-Signatureheaderstringt=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body" with the alert’s secret>. Refuse it if t is over 5 minutes off.
Earful-Event optionalheaderstring
Earful-Delivery optionalheaderstringThe delivery’s number.

Body

FieldTypeAbout
idstringThe event’s id (evt_…); a replay sends the same one, so dedupe on it.
eventany
livemodebooleanfalse when it comes from the test-mode sandbox (an ef_test_ key’s workspace).
createdAtstring
workspaceobject
idinteger
namestring
profileProfile

Responses

  • 200 Any 2xx counts as delivered; anything else is retried.

schemas

Problem

An RFC 9457 problem details object, sent as application/problem+json.

FieldTypeAbout
typestringA URI for the problem type; always about:blank, so title is the HTTP status text.
titlestringThe HTTP status text.
statusintegerThe HTTP status code.
detailstringWhat went wrong, in plain English, and usually how to fix it.
existingIdstring optionalOn a 409 for a profile that’s already there: its id.

Location

One business (or app) across all its review sites. Plans are priced per location.

FieldTypeAbout
idstringLocation id.
namestringThe business or branch name.
profileCountintegerHow many review pages this location has.
createdAtstringWhen it was added.
externalRefstring or nullYour own reference for it, like your store id. Filter with GET /locations?externalRef=.

Profile

One review page of one location on one site.

FieldTypeAbout
idstringProfile id.
locationIdstringThe location it belongs to.
locationNamestring or nullThat location’s name.
sitestring: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterraWhich review site.
namestringA name for the review page.
urlstringThe review page.
reviewCountintegerReviews held (not counting removed ones).
averageRatingnumber or nullAverage star rating of those reviews.
lastCheckedAtstring or nullWhen reviews were last collected.
nextCheckAtstringWhen the next collection is due.
lastErrorstring or nullWhy the last collection failed, if it did.
createdAtstringWhen it was added.
externalIdstring or nullThe listing’s own id on its site: a Google cid or place id, a Tripadvisor location id (d7106969), a Trustpilot domain, an App Store id, a Play package, a Booking.com hotel slug. Two links to one listing share it, so it’s what to match on, not url. null where the link doesn’t carry one.
importobjectIts history import. The first reviews come within a minute or two; then older ones load in the background, up to the plan’s cap (Free 40, Plus 5,000). A profile.imported webhook says when it’s over.
statusstring: queued | importing | done | failedqueued: first import not in yet. importing: loading older reviews. done or failed: over.
reviewsintegerReviews held so far.
limitinteger or nullThe history cap it ran to, once over.
finishedAtstring or nullWhen it was over.
errorstring or nullWhy it failed, if it did.

Review

FieldTypeAbout
idstringEarful’s id for the review.
sitestring: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterra
profileIdstringThe profile it came from.
profileNamestring or null
reviewIdstringThe site’s own id for the review.
authorstring or null
ratinginteger or nullWhole stars; out-of-10 sites are halved.
titlestring or null
textstring or null
languagestring or null
publishedAtstring or nullWhen the reviewer posted it.
updatedAtstring or nullWhen the site says it was last changed.
ownerReplynull or objectThe business’s reply, from the site or recorded in Earful.
reviewUrlstring or null
editedAtstring or nullWhen Earful noticed the reviewer edit it.
deletedAtstring or nullWhen Earful noticed it removed from the site.
handledAtstring or nullWhen it was marked handled.
firstSeenAtstringWhen Earful first collected it.
tagsarray of string
draftstring or nullThe reply draft, if there is one.
sentimentstring or null: positive | neutral | negative | mixedRead by AI when the review arrived; null until then or with AI off.
translationstring or nullAn English translation by AI, for reviews in another language.
appVersionstring or nullApp stores: the app version the review was left on.
countrystring or nullApp stores: the store country, ISO 3166 (alpha-2 from public feeds, alpha-3 from App Store Connect).
historybooleanCame in with the profile’s first import or history backfill, so no alert went out for it.

ReviewDetail

FieldTypeAbout
idstring optionalEarful’s id for the review.
sitestring: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterra optional
profileIdstring optionalThe profile it came from.
profileNamestring or null optional
reviewIdstring optionalThe site’s own id for the review.
authorstring or null optional
ratinginteger or null optionalWhole stars; out-of-10 sites are halved.
titlestring or null optional
textstring or null optional
languagestring or null optional
publishedAtstring or null optionalWhen the reviewer posted it.
updatedAtstring or null optionalWhen the site says it was last changed.
ownerReplynull or object optionalThe business’s reply, from the site or recorded in Earful.
reviewUrlstring or null optional
editedAtstring or null optionalWhen Earful noticed the reviewer edit it.
deletedAtstring or null optionalWhen Earful noticed it removed from the site.
handledAtstring or null optionalWhen it was marked handled.
firstSeenAtstring optionalWhen Earful first collected it.
tagsarray of string optional
draftstring or null optionalThe reply draft, if there is one.
sentimentstring or null: positive | neutral | negative | mixed optionalRead by AI when the review arrived; null until then or with AI off.
translationstring or null optionalAn English translation by AI, for reviews in another language.
appVersionstring or null optionalApp stores: the app version the review was left on.
countrystring or null optionalApp stores: the store country, ISO 3166 (alpha-2 from public feeds, alpha-3 from App Store Connect).
historyboolean optionalCame in with the profile’s first import or history backfill, so no alert went out for it.
replyRouteobject optional
modestring: api | link | noneapi: POST /reply posts to the site. link: post it on the site at url. none: no way to reply.
viastring or nullWhich owner API posts it (appstore, googleplay, gbp) when mode is api.
urlstring or nullThe site’s reply page when mode is link.

Draft

FieldTypeAbout
textstringThe draft reply.
aiDraftsLeftinteger or nullAI drafts left this month; null when unlimited or not an AI draft.

ReplyResult

FieldTypeAbout
postedToSitebooleantrue when Earful posted it through the site’s owner API.
viastring: appstore | googleplay | gbp | facebook | manualmanual means recorded only: post it on the site yourself.
replyUrlstring or nullWhere to post it when via is manual.
reviewReviewDetail

Alert

FieldTypeAbout
idstringAlert id.
channelstring: digest | email | slack | webhook
frequencystring or null: daily | weeklyFor digests.
targetstringWhere it goes: the email address, “Slack channel”, or the webhook’s host. Full URLs stay encrypted.
maxRatinginteger or nullOnly reviews at or below this rating; null for all.
sitesarray of string: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterraOnly these sites; empty for all.
lastSentAtstring or nullWhen it last sent.
lastErrorstring or null
createdAtstringWhen it was added.
locationIdstring or nullOnly this location; null for all.
sentimentsarray of string: positive | neutral | negative | mixedOnly reviews the AI read as one of these; empty for any.
tagsarray of stringOnly reviews with one of these tags; empty for any.
keywordsarray of stringOnly reviews that mention one of these (in the text or its translation); empty for any.

NewAlert

FieldTypeAbout
idstring optionalAlert id.
channelstring: digest | email | slack | webhook optional
frequencystring or null: daily | weekly optionalFor digests.
targetstring optionalWhere it goes: the email address, “Slack channel”, or the webhook’s host. Full URLs stay encrypted.
maxRatinginteger or null optionalOnly reviews at or below this rating; null for all.
sitesarray of string: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterra optionalOnly these sites; empty for all.
lastSentAtstring or null optionalWhen it last sent.
lastErrorstring or null optional
createdAtstring optionalWhen it was added.
locationIdstring or null optionalOnly this location; null for all.
sentimentsarray of string: positive | neutral | negative | mixed optionalOnly reviews the AI read as one of these; empty for any.
tagsarray of string optionalOnly reviews with one of these tags; empty for any.
keywordsarray of string optionalOnly reviews that mention one of these (in the text or its translation); empty for any.
secretstring or null optionalA webhook’s signing secret. Shown only in this answer.

ApiKey

FieldTypeAbout
idintegerKey id.
namestringWhat it’s for.
scopestring: read | writeread: GET only. write: everything.
prefixstringThe start of the key, to tell keys apart.
createdAtstringWhen it was made.
expiresAtstring or nullWhen it stops working; null for never. A rotated key’s grace period ends here.
lastUsedAtstring or nullWhen it was last used.
lastUsedIpstring or nullThe address it was last used from.
requestCountintegerRequests made with it.
modestring: live | testtest: an ef_test_ key, which reaches the workspace’s sandbox (made-up data, no outside calls).

NewApiKey

FieldTypeAbout
idinteger optionalKey id.
namestring optionalWhat it’s for.
scopestring: read | write optionalread: GET only. write: everything.
prefixstring optionalThe start of the key, to tell keys apart.
createdAtstring optionalWhen it was made.
expiresAtstring or null optionalWhen it stops working; null for never. A rotated key’s grace period ends here.
lastUsedAtstring or null optionalWhen it was last used.
lastUsedIpstring or null optionalThe address it was last used from.
requestCountinteger optionalRequests made with it.
modestring: live | test optionaltest: an ef_test_ key, which reaches the workspace’s sandbox (made-up data, no outside calls).
keystring optionalThe key itself. Shown only in this answer; Earful keeps a hash.

RotatedApiKey

FieldTypeAbout
previousApiKey optional

Workspace

FieldTypeAbout
idintegerWorkspace id.
namestringWorkspace name.
businessNamestring or nullThe name AI drafts sign as.
tonestring: friendly | professional | playful | concise
voiceNotesstring or null
signoffstring or null
planobject
namestring: free | pro
locationsintegerLocations the plan covers.
checkEveryMinutesintegerHow often reviews are collected.
aiDraftsPerMonthinteger or nullnull for unlimited.
alertChannelsarray of string
directRepliesbooleanWhether replies post straight to sites with an owner API.
addonsobjectHow many of the plan’s locations the Local add-on (map rank points) covers.
requestEmailsPerMonthinteger
requestTextsPerMonthinteger
rankPointsPerMonthintegerMap rank grid points a month.
appTopicsbooleanWhether AI topics for apps are included.
usageobject
locationsintegerLocations in use.
profilesintegerReview pages in use.
aiDraftsThisMonthintegerAI drafts made this calendar month.
aiDraftsLeftinteger or nullnull for unlimited.
requestEmailsThisMonthintegerSent and queued this calendar month.
requestTextsThisMonthinteger
rankPointsThisMonthinteger
autoReplyobjectAuto-replies: see reply rules.
enabledbooleanOff until you turn it on.
dailyCapintegerMost replies posted automatically in 24 hours.

Candidate

FieldTypeAbout
candidateIdintegerPass this to POST /locations/{id}/profiles to add it.
sitestring: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterra
namestringThe business or app.
addressstring or nullStreet address; for apps, the developer.
urlstringIts review page.
ratingnumber or null
reviewCountinteger or null

PlaceSearch

FieldTypeAbout
idintegerSearch id.
querystringWhat was searched for.
nearstring or null
statusstring: running | done | faileddone once every site has finished (some may have failed); failed when all did.
createdAtstringWhen it started.
sitesarray of object
candidatesarray of Candidate

TagRule

FieldTypeAbout
idinteger
tagstringLower case letters, digits, - and _.
descriptionstringWhen the AI should apply it.
createdAtstring

ReplyRule

FieldTypeAbout
idinteger
locationIdstring or nullThe location it covers; null for every location.
minRatinginteger
maxRatinginteger
sitesarray of string: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterraEmpty for every site.
languagesarray of stringTwo-letter codes; empty for any.
keywordsarray of stringThe review must mention one; empty for any.
actionstring: post | approvepost: post the AI reply where the site has a reply API, otherwise hold it. approve: always hold it.
createdAtstring

AutoReply

FieldTypeAbout
idstringAuto-reply id.
reviewIdstringThe review.
ruleIdinteger or null
statusstring: pending | posted | approved | rejected | skippedpending is the approval queue; posted means posted automatically.
textstring or nullThe AI reply (as approved, if edited).
reasonstring or nullWhy it’s held or skipped.
decidedBystring or nullauto, an email, an API key or a Slack user.
createdAtstring
decidedAtstring or null
reviewobject
sitestring: google | tripadvisor | trustpilot | booking | appstore | googleplay | feefo | reviewsio | yelp | deliveroo | justeat | ubereats | opentable | doordash | grubhub | facebook | g2 | capterra
ratinginteger or null
authorstring or null
textstring or null
profileNamestring

ConnectedApp

An app someone connected to the workspace with OAuth, like a claude.ai connector to the MCP server.

FieldTypeAbout
idintegerConnection id.
namestringThe app’s name, as it registered.
clientIdstringIts OAuth client id: efc_… when it registered itself, or the https address of its Client ID Metadata Document.
redirectHostsarray of stringWhere it sends people back to after sign-in.
scopestring: read | writeread: search and read. write: also draft, post and add.
connectedBystringWho allowed it.
createdAtstringWhen it was connected.
lastUsedAtstring or nullWhen it last called the MCP server.
lastUsedIpstring or nullThe address it last called from.
requestCountintegerMCP requests it has made.

ReviewRequest

FieldTypeAbout
idinteger
locationIdstringThe location.
locationNamestring or null
channelstring: email | sms
contactstringEmail address, or mobile number in E.164.
namestring or null
orderRefstring or null
sourcestring: api | csv | webhook | app
statusstring: queued | sent | failed | suppressedsuppressed: the customer opted out before it went.
sendAtstring
sentAtstring or null
remindedAtstring or nullWhen the one reminder went, to people who hadn’t clicked.
clickedAtstring or nullFirst click on the review link.
reviewIdstring or nullThe review Earful matched to this request by the reviewer’s name.
unsubscribedAtstring or null
errorstring or null
transactedAtstring or null
createdAtstring

ReviewRequestSettings

FieldTypeAbout
emailSubjectstring
emailBodystringMust contain {link}. {name} and {business} are filled in.
reminderSubjectstring
reminderBodystring
smsBodystring
reminderDaysinteger0 for no reminder.
replyTostring or null
acknowledgedbooleanWhether the owner agreed the sending rules; requests need it.
acknowledgedAtstring or null
consentWordingstringSuggested wording for receipts and booking forms.

Suppression

FieldTypeAbout
idinteger
contactstring
reasonstring: unsubscribed | manual
createdAtstring

App

FieldTypeAbout
idstringThe App Store or Google Play profile’s id.
locationIdstringThe location.
sitestring: appstore | googleplay
namestring
urlstring
reviewCountinteger
averageRatingnumber or null
latestVersionstring or null

AppTopics

FieldTypeAbout
appIdstringThe app’s profile id.
reviewCountintegerReviews the topics were drawn from (the newest written ones).
topicsarray of object
createdAtstring

RankScan

FieldTypeAbout
idinteger
trackerIdinteger
statusstring: running | done | failed
pointsintegerGrid points, which count against the plan’s monthly allowance.
averageRanknumber or nullAverage Maps rank over the points where the business shows.
top3Sharenumber or nullShare of points (0–1) where it’s in the top 3.
foundSharenumber or nullShare of points (0–1) where it’s in the top 20.
costUsdnumber or null
createdAtstring
finishedAtstring or null

RankTracker

FieldTypeAbout
idinteger
locationIdstringThe location.
keywordstring
gridSizeinteger: 3 | 5 | 7 | 9 | 11 | 13
radiusKmnumber
centerobject
latnumber
lngnumber
businessobject
cidstring or null
namestring
everystring or null: week | fortnight | month
nextScanAtstring or null
createdAtstring
scansarray of RankScanNewest first.

RankScanDetail

FieldTypeAbout
idinteger optional
trackerIdinteger optional
statusstring: running | done | failed optional
pointsinteger optionalGrid points, which count against the plan’s monthly allowance.
averageRanknumber or null optionalAverage Maps rank over the points where the business shows.
top3Sharenumber or null optionalShare of points (0–1) where it’s in the top 3.
foundSharenumber or null optionalShare of points (0–1) where it’s in the top 20.
costUsdnumber or null optional
createdAtstring optional
finishedAtstring or null optional
competitorsarray of object optional
gridarray of object optionalRow by row from the north-west corner.

WebhookDelivery

One webhook send: what went, to which alert, and how it went.

FieldTypeAbout
idstringDelivery id.
alertIdstringThe webhook alert it went to.
eventstring: reviews.new | profile.importedThe event.
statusstring: pending | delivered | failedpending: will retry at nextAttemptAt.
testbooleanA test send (POST /alerts/{id}/test); never retried.
attemptsintegerTries so far.
responseStatusinteger or nullYour endpoint’s last HTTP status.
errorstring or nullWhy the last try failed.
replayOfstring or nullThe delivery this replays.
createdAtstring
lastAttemptAtstring or null
nextAttemptAtstring or nullThe next retry, while pending.
deliveredAtstring or null

ReviewsNewEvent

New reviews that match the alert’s filters.

FieldTypeAbout
idstringThe event’s id (evt_…); a replay sends the same one, so dedupe on it.
eventany
livemodebooleanfalse when it comes from the test-mode sandbox (an ef_test_ key’s workspace).
createdAtstring
workspaceobject
idinteger
namestring
reviewsarray of Review

ProfileImportedEvent

A profile’s history import is over (import.status done or failed).

FieldTypeAbout
idstringThe event’s id (evt_…); a replay sends the same one, so dedupe on it.
eventany
livemodebooleanfalse when it comes from the test-mode sandbox (an ef_test_ key’s workspace).
createdAtstring
workspaceobject
idinteger
namestring
profileProfile