Profile Comments API Reference
The Profile Comments API exposes feedback that is scoped to a profile and location, rather than to an individual post. It surfaces Google Business Profile reviews and Facebook Page recommendations — both live on a location or Page, not on a post, so they cannot be expressed through the post-level Comments API.
All write operations are processed asynchronously.
Endpoints
Section titled “Endpoints”| Method | Endpoint | Description |
|---|---|---|
GET | /api/profiles/:profile_id/comments | List top-level comments + their replies |
GET | /api/profiles/:profile_id/comments/:id | Get a single comment |
POST | /api/profiles/:profile_id/comments | Create a reply to an existing comment |
DELETE | /api/profiles/:profile_id/comments/:id | Delete your reply (the original review stays) |
Comment ID resolution
Section titled “Comment ID resolution”Endpoints that accept a comment :id in the path, and the parent_id field on POST, accept either:
- Postproxy ID: the comment’s hashid (e.g.
abc123xyz) - External ID: the platform’s native ID — Google’s resource path (e.g.
accounts/1234/locations/5678/reviews/AbFvOq) or Facebook’s recommendation story ID (e.g.122219817518532572)
Sandbox
Section titled “Sandbox”With a sandbox key, review replies and deletes are answered by sandbox plugins, and profile_comment.created reaches your sandbox webhook endpoints — without touching the engagement quota. Incoming Google Business reviews and Facebook recommendations are fabricated from the Sandbox page in the dashboard.
Platform support
Section titled “Platform support”| Action | Google Business | |
|---|---|---|
| List | Yes (reviews on the location) | Yes (recommendations on the Page) |
| Reply | Yes (one reply per review) | Yes (a comment on the recommendation; several allowed) |
| Delete | Yes (removes your reply; the review stays) | Yes (removes your comment; the recommendation stays) |
Top-level comments cannot be authored — reviews come from end users. POST requests must supply a parent_id pointing at the review you are replying to.
Google Business specifics
Section titled “Google Business specifics”platform_data.review_reply_urlon a review is Google’s “reply to this review” link for the business owner.- Reviews removed on Google (by the reviewer or by Google moderation) and owner replies deleted outside Postproxy are marked
status: "deleted"on the next sync. Deleted rows drop out of list responses but remain fetchable by id, and aprofile_comment.deletedwebhook fires for each one. - Replies carry
platform_data.reply_state(PENDING,APPROVED, orREJECTED) — Google moderates owner replies. When the state isREJECTED,platform_data.policy_violationnames the reason (e.g.FAKE_ENGAGEMENT,OFF_TOPIC,PERSONAL_INFO). Both come back on thePOSTreply response once published and are refreshed on every sync.
Facebook specifics
Section titled “Facebook specifics”platform_data.recommendation_typeis"positive"or"negative".platform_data.star_ratingis only present on legacy reviews left before Facebook replaced stars with recommendations (2018).- Meta does not expose who left a recommendation —
author_usernameandauthor_avatar_urlare alwaysnullon Facebook rows. placement_idis the Page ID.
Permalink
Section titled “Permalink”Google rows point to the location’s reviews page on Google (https://search.google.com/local/reviews?placeid=...); Facebook rows point to the Page’s reviews tab (https://www.facebook.com/<page_id>/reviews). The link is shared by all reviews and replies on that location / Page — neither platform exposes per-review URLs. permalink is null for Google when the location’s placeId is unavailable.
Sync cadence
Section titled “Sync cadence”Google Business reviews are pulled from Google hourly for every active, eligible google_business profile.
Facebook recommendations are backfilled once when the profile connects, then kept current in real time — new recommendations, edits, removals, and comments on them land within seconds.
List comments
Section titled “List comments”GET /api/profiles/:profile_id/comments
Retrieves a paginated list of top-level comments for a profile. Each top-level comment includes a replies array containing all nested replies sorted by created_at ascending.
Path parameters
Section titled “Path parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
profile_id | string | Yes | Profile ID (hashid) |
Query parameters
Section titled “Query parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
placement_id | string | No | - | Filter to comments on a single location / Page (the accounts/X/locations/Y path or Page ID returned by List Placements) |
page | integer | No | 1 | Page number (1-based). Passing 0 returns page 1 |
per_page | integer | No | 20 | Number of top-level comments per page |
Results are sorted by posted_at descending (newest first). Records without a posted_at are returned last.
Google Business reviews can be star-only (no text). For these, body is null while platform_data.star_rating is still populated — don’t skip records on a null body.
Example
Section titled “Example”curl -X GET "https://api.postproxy.dev/api/profiles/PROFILE_ID/comments?page=1&per_page=20" \ -H "Authorization: Bearer YOUR_API_KEY"import PostProxy from "postproxy-sdk";
const client = new PostProxy("YOUR_API_KEY");const reviews = await client.profileComments.list("PROFILE_ID");console.log(`Total reviews: ${reviews.total}`);for (const review of reviews.data) { console.log(`★${review.platform_data?.star_rating ?? "—"} ${review.author_username}: ${review.body}`); for (const reply of review.replies) { console.log(` reply: ${reply.body}`); }}package main
import ( "context" "fmt" postproxy "github.com/postproxy/postproxy-go")
func main() { client := postproxy.NewClient("YOUR_API_KEY") reviews, _ := client.ProfileComments.List(context.Background(), "PROFILE_ID", nil) fmt.Printf("Total reviews: %d\n", reviews.Total) for _, r := range reviews.Data { fmt.Printf("%v: %s\n", r.AuthorUsername, r.Body) for _, reply := range r.Replies { fmt.Printf(" reply: %s\n", reply.Body) } }}require "postproxy"
client = PostProxy::Client.new("YOUR_API_KEY")reviews = client.profile_comments.list("PROFILE_ID")puts "Total reviews: #{reviews.total}"reviews.data.each do |review| puts "#{review.author_username}: #{review.body}" review.replies.each do |reply| puts " reply: #{reply.body}" endenduse PostProxy\Client;
$client = new Client("YOUR_API_KEY");$reviews = $client->profileComments()->list("PROFILE_ID");echo "Total reviews: {$reviews->total}\n";foreach ($reviews->data as $review) { echo "{$review->authorUsername}: {$review->body}\n"; foreach ($review->replies as $reply) { echo " reply: {$reply->body}\n"; }}import dev.postproxy.sdk.PostProxy;
var client = PostProxy.builder("YOUR_API_KEY").build();var reviews = client.profileComments().list("PROFILE_ID");System.out.println("Total reviews: " + reviews.total());for (var review : reviews.data()) { System.out.println(review.authorUsername() + ": " + review.body()); for (var reply : review.replies()) { System.out.println(" reply: " + reply.body()); }}using PostProxy;
var client = PostProxyClient.Builder("YOUR_API_KEY").Build();var reviews = await client.ProfileComments.ListAsync("PROFILE_ID");Console.WriteLine($"Total reviews: {reviews.Total}");foreach (var review in reviews.Data){ Console.WriteLine($"{review.AuthorUsername}: {review.Body}"); if (review.Replies is not null) { foreach (var reply in review.Replies) { Console.WriteLine($" reply: {reply.Body}"); } }}Response:
{ "total": 2, "page": 1, "per_page": 20, "data": [ { "id": "abc123", "external_id": "accounts/1234/locations/5678/reviews/AbFvOq", "parent_external_id": null, "placement_id": "accounts/1234/locations/5678", "body": "Great coffee, friendly staff!", "status": "synced", "author_username": "Jane D.", "author_avatar_url": "https://lh3.googleusercontent.com/...", "platform_data": { "star_rating": 5, "update_time": "2026-05-10T12:00:00Z", "review_reply_url": "https://business.google.com/..." }, "permalink": "https://search.google.com/local/reviews?placeid=ChIJxyz", "posted_at": "2026-05-10T11:55:00Z", "created_at": "2026-05-13T06:00:01Z", "replies": [ { "id": "def456", "external_id": "accounts/1234/locations/5678/reviews/AbFvOq/reply", "parent_external_id": "accounts/1234/locations/5678/reviews/AbFvOq", "placement_id": "accounts/1234/locations/5678", "body": "Thanks Jane — see you again soon!", "status": "published", "author_username": null, "author_avatar_url": null, "platform_data": { "reply_state": "APPROVED" }, "permalink": "https://search.google.com/local/reviews?placeid=ChIJxyz", "posted_at": "2026-05-12T15:00:00Z", "created_at": "2026-05-12T15:00:01Z" } ] } ]}Response fields
Section titled “Response fields”| Field | Type | Description |
|---|---|---|
total | integer | Total number of top-level comments |
page | integer | Current page number |
per_page | integer | Items per page |
data | array | Array of top-level comment objects, each with a replies array |
Reply nesting
Section titled “Reply nesting”Pagination applies to top-level comments only. All replies are flattened into the replies array of their root top-level comment, sorted by created_at ascending. Each reply retains its parent_external_id so the client can reconstruct the tree if needed.
Get comment
Section titled “Get comment”GET /api/profiles/:profile_id/comments/:id
Retrieves a single comment with its direct replies.
Path parameters
Section titled “Path parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
profile_id | string | Yes | Profile ID |
id | string | Yes | Comment ID or external ID |
Example
Section titled “Example”curl -X GET "https://api.postproxy.dev/api/profiles/PROFILE_ID/comments/COMMENT_ID" \ -H "Authorization: Bearer YOUR_API_KEY"import PostProxy from "postproxy-sdk";
const client = new PostProxy("YOUR_API_KEY");const review = await client.profileComments.get("PROFILE_ID", "COMMENT_ID");console.log(`${review.author_username}: ${review.body}`);package main
import ( "context" "fmt" postproxy "github.com/postproxy/postproxy-go")
func main() { client := postproxy.NewClient("YOUR_API_KEY") review, _ := client.ProfileComments.Get(context.Background(), "PROFILE_ID", "COMMENT_ID") fmt.Printf("%v: %s\n", review.AuthorUsername, review.Body)}require "postproxy"
client = PostProxy::Client.new("YOUR_API_KEY")review = client.profile_comments.get("PROFILE_ID", "COMMENT_ID")puts "#{review.author_username}: #{review.body}"use PostProxy\Client;
$client = new Client("YOUR_API_KEY");$review = $client->profileComments()->get("PROFILE_ID", "COMMENT_ID");echo "{$review->authorUsername}: {$review->body}\n";import dev.postproxy.sdk.PostProxy;
var client = PostProxy.builder("YOUR_API_KEY").build();var review = client.profileComments().get("PROFILE_ID", "COMMENT_ID");System.out.println(review.authorUsername() + ": " + review.body());using PostProxy;
var client = PostProxyClient.Builder("YOUR_API_KEY").Build();var review = await client.ProfileComments.GetAsync("PROFILE_ID", "COMMENT_ID");Console.WriteLine($"{review.AuthorUsername}: {review.Body}");Response:
{ "id": "abc123", "external_id": "accounts/1234/locations/5678/reviews/AbFvOq", "parent_external_id": null, "placement_id": "accounts/1234/locations/5678", "body": "Great coffee, friendly staff!", "status": "synced", "author_username": "Jane D.", "author_avatar_url": "https://lh3.googleusercontent.com/...", "platform_data": { "star_rating": 5, "update_time": "2026-05-10T12:00:00Z", "review_reply_url": "https://business.google.com/..." }, "permalink": "https://search.google.com/local/reviews?placeid=ChIJxyz", "posted_at": "2026-05-10T11:55:00Z", "created_at": "2026-05-13T06:00:01Z", "replies": [ { "id": "def456", "external_id": "accounts/1234/locations/5678/reviews/AbFvOq/reply", "parent_external_id": "accounts/1234/locations/5678/reviews/AbFvOq", "placement_id": "accounts/1234/locations/5678", "body": "Thanks Jane — see you again soon!", "status": "published", "author_username": null, "author_avatar_url": null, "platform_data": { "reply_state": "APPROVED" }, "permalink": "https://search.google.com/local/reviews?placeid=ChIJxyz", "posted_at": "2026-05-12T15:00:00Z", "created_at": "2026-05-12T15:00:01Z" } ]}Reply to a comment
Section titled “Reply to a comment”POST /api/profiles/:profile_id/comments
Creates a reply to an existing review. The reply is stored immediately with status: "pending" and external_id: null; it is then published to the platform and the status updates to published once it lands (or failed / failed_waiting_for_retry with error and error_details populated).
On Google Business a review can carry one reply; on Facebook a recommendation is replied to with a comment, and several are allowed.
Top-level comments cannot be created — the request returns 422 if parent_id is missing.
Request body
Section titled “Request body”| Parameter | Type | Required | Description |
|---|---|---|---|
parent_id | string | Yes | Postproxy ID or external ID of the review being replied to |
body | string | Yes | Reply body. (The legacy text parameter is still accepted as an alias.) |
Example
Section titled “Example”curl -X POST "https://api.postproxy.dev/api/profiles/PROFILE_ID/comments" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "parent_id": "accounts/1234/locations/5678/reviews/AbFvOq", "body": "Thanks for the kind words!" }'import PostProxy from "postproxy-sdk";
const client = new PostProxy("YOUR_API_KEY");const reply = await client.profileComments.create( "PROFILE_ID", "accounts/1234/locations/5678/reviews/AbFvOq", "Thanks for the kind words!",);console.log(`Reply created with status: ${reply.status}`);package main
import ( "context" "fmt" postproxy "github.com/postproxy/postproxy-go")
func main() { client := postproxy.NewClient("YOUR_API_KEY") reply, _ := client.ProfileComments.Create( context.Background(), "PROFILE_ID", "accounts/1234/locations/5678/reviews/AbFvOq", "Thanks for the kind words!", ) fmt.Printf("Reply created with status: %s\n", reply.Status)}require "postproxy"
client = PostProxy::Client.new("YOUR_API_KEY")reply = client.profile_comments.create( "PROFILE_ID", parent_id: "accounts/1234/locations/5678/reviews/AbFvOq", body: "Thanks for the kind words!")puts "Reply created with status: #{reply.status}"use PostProxy\Client;
$client = new Client("YOUR_API_KEY");$reply = $client->profileComments()->create( "PROFILE_ID", "accounts/1234/locations/5678/reviews/AbFvOq", "Thanks for the kind words!",);echo "Reply created with status: {$reply->status}\n";import dev.postproxy.sdk.PostProxy;
var client = PostProxy.builder("YOUR_API_KEY").build();var reply = client.profileComments().create( "PROFILE_ID", "accounts/1234/locations/5678/reviews/AbFvOq", "Thanks for the kind words!");System.out.println("Reply created with status: " + reply.status());using PostProxy;
var client = PostProxyClient.Builder("YOUR_API_KEY").Build();var reply = await client.ProfileComments.CreateAsync( "PROFILE_ID", "accounts/1234/locations/5678/reviews/AbFvOq", "Thanks for the kind words!");Console.WriteLine($"Reply created with status: {reply.Status}");Response (201 Created):
{ "id": "ghi789", "external_id": null, "parent_external_id": "accounts/1234/locations/5678/reviews/AbFvOq", "placement_id": "accounts/1234/locations/5678", "body": "Thanks for the kind words!", "status": "pending", "author_username": null, "author_avatar_url": null, "platform_data": null, "posted_at": null, "created_at": "2026-05-13T10:30:00Z"}Delete comment
Section titled “Delete comment”DELETE /api/profiles/:profile_id/comments/:id
Deletes your reply to a review. Neither platform lets businesses delete reviews themselves — the original review row stays.
Path parameters
Section titled “Path parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
profile_id | string | Yes | Profile ID |
id | string | Yes | Comment ID or external ID |
Example
Section titled “Example”curl -X DELETE "https://api.postproxy.dev/api/profiles/PROFILE_ID/comments/COMMENT_ID" \ -H "Authorization: Bearer YOUR_API_KEY"import PostProxy from "postproxy-sdk";
const client = new PostProxy("YOUR_API_KEY");const result = await client.profileComments.delete("PROFILE_ID", "COMMENT_ID");console.log(`Delete accepted: ${result.accepted}`);package main
import ( "context" "fmt" postproxy "github.com/postproxy/postproxy-go")
func main() { client := postproxy.NewClient("YOUR_API_KEY") result, _ := client.ProfileComments.Delete(context.Background(), "PROFILE_ID", "COMMENT_ID") fmt.Printf("Delete accepted: %v\n", result.Accepted)}require "postproxy"
client = PostProxy::Client.new("YOUR_API_KEY")result = client.profile_comments.delete("PROFILE_ID", "COMMENT_ID")puts "Delete accepted: #{result.accepted}"use PostProxy\Client;
$client = new Client("YOUR_API_KEY");$result = $client->profileComments()->delete("PROFILE_ID", "COMMENT_ID");echo "Delete accepted: " . ($result->accepted ? "true" : "false") . "\n";import dev.postproxy.sdk.PostProxy;
var client = PostProxy.builder("YOUR_API_KEY").build();var result = client.profileComments().delete("PROFILE_ID", "COMMENT_ID");System.out.println("Delete accepted: " + result.accepted());using PostProxy;
var client = PostProxyClient.Builder("YOUR_API_KEY").Build();var result = await client.ProfileComments.DeleteAsync("PROFILE_ID", "COMMENT_ID");Console.WriteLine($"Delete accepted: {result.Accepted}");Response (200 OK):
{ "accepted": true}Webhooks
Section titled “Webhooks”The profile_comment.created event fires whenever a new profile comment record appears — both for newly synced incoming reviews and for outgoing replies that have just been published. Subscribe to profile_comment.created (or *) under your webhook endpoint’s events list. See the Webhooks reference for delivery, retries, and signature verification.
profile_comment.created fires once per new review across all locations of the account. Webhook registration does not currently offer a placement filter — subscribers with multi-location accounts should filter on data.object.placement_id themselves.
profile_comment.deleted carries the same payload with status: "deleted" and fires when a synced Google Business review or owner reply is no longer present on Google. Subscribe to it (or *) to keep a mirror in sync.
Sample payload (data.object holds the comment; see Webhooks · Event payload for the envelope):
{ "id": "whevt_p9k4j7", "object": "event", "type": "profile_comment.created", "created_at": "2026-05-13T06:00:01Z", "data": { "object": { "id": "abc123", "profile_id": "prof123abc", "platform": "google_business", "placement_id": "accounts/1234/locations/5678", "external_id": "accounts/1234/locations/5678/reviews/AbFvOq", "parent_external_id": null, "body": "Great coffee, friendly staff!", "status": "synced", "error": null, "error_details": null, "author_username": "Jane D.", "author_avatar_url": "https://lh3.googleusercontent.com/...", "platform_data": { "star_rating": 5, "update_time": "2026-05-10T12:00:00Z", "review_reply_url": "https://business.google.com/..." }, "permalink": "https://search.google.com/local/reviews?placeid=ChIJxyz", "posted_at": "2026-05-10T11:55:00Z", "created_at": "2026-05-13T06:00:01Z" } }}Profile comment object fields
Section titled “Profile comment object fields”| Field | Type | Description |
|---|---|---|
id | string | Postproxy comment ID (hashid) |
external_id | string|null | Platform’s native resource ID (null while a reply is pending publication) |
parent_external_id | string|null | External ID of parent review (null for top-level reviews) |
placement_id | string | Location path (e.g. accounts/X/locations/Y) for Google Business; Page ID for Facebook |
body | string | Comment/review text |
status | string | One of synced, pending, published, failed, failed_waiting_for_retry, deleted |
error | string|null | Error summary if an outgoing reply failed (null otherwise) |
error_details | object|null | Structured platform error (omitted when no platform error info is available) |
error_details.platform_error_code | string|null | Error code returned by the platform API |
error_details.platform_error_subcode | string|null | Error subcode returned by the platform API |
error_details.platform_error_message | string|null | Error message returned by the platform API |
error_details.postproxy_note | string|null | Additional context from Postproxy about the error |
author_username | string|null | Display name of reviewer (null for your own replies, and always null on Facebook) |
author_avatar_url | string|null | Profile image URL |
platform_data | object|null | Platform-specific metadata (e.g. star_rating, update_time, review_reply_url for Google reviews; reply_state and policy_violation on Google replies — see Google Business specifics; recommendation_type for Facebook recommendations) |
permalink | string|null | URL to the comment on the platform (the location’s reviews page on Google or the Page’s reviews tab on Facebook; null if unavailable) |
posted_at | string|null | ISO 8601 timestamp of platform posting |
created_at | string | ISO 8601 timestamp of record creation |
replies | array | Nested replies to this comment (present on list/get responses) |
Statuses
Section titled “Statuses”| Status | Description |
|---|---|
synced | Review fetched from the platform during sync |
pending | Reply created via API, awaiting publication |
published | Reply successfully posted to the platform |
failed | Reply failed to publish |
failed_waiting_for_retry | Reply failed and is queued for retry |
deleted | Review or reply no longer exists on the platform (Google Business). Excluded from list responses, still fetchable by id |
Error responses
Section titled “Error responses”Missing parent_id (422):
{ "error": "parent_id is required"}Unsupported method (405):
{ "error": "Unsupported method for instagram"}Not found (404):
{ "error": "Not found"}Bad request (400):
{ "error": "param is missing or the value is empty"}