Profile Comments API Reference
The Profile Comments API exposes feedback that is scoped to a profile and location, rather than to an individual post. Today it surfaces Google Business Profile reviews — reviews live on a location, 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 resource path (e.g.
accounts/1234/locations/5678/reviews/AbFvOq)
Platform support
Section titled “Platform support”| Action | Google Business |
|---|---|
| List | Yes (reviews on the location) |
| Reply | Yes (as a reply to an existing review only) |
| Delete | Yes (removes your reply; the review stays) |
Top-level comments cannot be authored — Google Business reviews come from end users. POST requests must supply a parent_id pointing at the review you are replying to.
Sync cadence
Section titled “Sync cadence”Reviews are pulled from Google twice a day (06:00 and 18:00 UTC) for every active, eligible google_business profile. The Profile#comments_synced_at timestamp tracks the most recent sync.
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 (the accounts/X/locations/Y path returned by List Placements) |
page | integer | No | 0 | Page number (zero-indexed) |
per_page | integer | No | 20 | Number of top-level comments per page |
Example
Section titled “Example”curl -X GET "https://api.postproxy.dev/api/profiles/PROFILE_ID/comments?page=0&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": 0, "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" }, "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": null, "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" }, "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": null, "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; a background job then calls the Google Business API and updates the status to published once it lands (or failed / failed_waiting_for_retry with error_message populated).
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 |
text | string | Yes | Reply body |
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", "text": "Thanks for the kind words!" }'import PostProxy from "postproxy-sdk";
const client = new PostProxy("YOUR_API_KEY");const reply = await client.profileComments.reply("PROFILE_ID", { parent_id: "accounts/1234/locations/5678/reviews/AbFvOq", text: "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.Reply(context.Background(), "PROFILE_ID", &postproxy.ProfileCommentReplyParams{ ParentID: "accounts/1234/locations/5678/reviews/AbFvOq", Text: "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.reply( "PROFILE_ID", parent_id: "accounts/1234/locations/5678/reviews/AbFvOq", text: "Thanks for the kind words!")puts "Reply created with status: #{reply.status}"use PostProxy\Client;
$client = new Client("YOUR_API_KEY");$reply = $client->profileComments()->reply("PROFILE_ID", [ "parent_id" => "accounts/1234/locations/5678/reviews/AbFvOq", "text" => "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().reply( "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.ReplyAsync("PROFILE_ID", new ProfileCommentReplyRequest{ ParentId = "accounts/1234/locations/5678/reviews/AbFvOq", Text = "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. The Google Business API does not let 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.
Sample payload:
{ "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", "author_username": "Jane D.", "author_avatar_url": "https://lh3.googleusercontent.com/...", "platform_data": { "star_rating": 5, "update_time": "2026-05-10T12:00:00Z" }, "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) |
body | string | Comment/review text |
status | string | One of synced, pending, published, failed, failed_waiting_for_retry |
author_username | string|null | Display name of reviewer (null for your own replies) |
author_avatar_url | string|null | Profile image URL |
platform_data | object|null | Platform-specific metadata (e.g. star_rating, update_time for Google reviews) |
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 |
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"}