Getting started
The Doop API lets an app act on behalf of a consenting account. API access is available to all active Doop accounts, with no paid subscription required. Documentation is public.
https://doop.live/api/v1v1- Open Settings → Integrations and create an app. Add your app’s privacy policy, support URL, and exact HTTPS callback URL.
- Save your client ID and the one-time client secret in your server’s secret store. Apps start with access to their owner’s account. Request review to connect other Doop accounts.
- Complete the OAuth flow below. Keep client secrets and tokens on your backend.
- Make your first request with the resulting access token.
curl "https://doop.live/api/v1/me" \
-H "Authorization: Bearer $DOOP_ACCESS_TOKEN"{
"data": {
"id": "9cae713a-e731-40be-8c77-53bdfab2fd60",
"username": "amara",
"display_name": "Amara Mensah"
}
}Examples use illustrative IDs and content. Replace them with values returned for your account. You can download the OpenAPI specification for API clients.
v1 supports confidential server-side OAuth clients. Personal API keys, attachments, bulk messaging, post publishing, uploads, analytics, and financial operations are not included.
OAuth & account connections
Use authorization code OAuth with PKCE S256. Each connection records the user’s explicit consent. Additional permissions require a new consent flow.
1. Prepare the authorization request
Create an unpredictable verifier and state value. Store both in the user’s server-side session, then redirect their browser to the authorization URL. The callback must exactly match a registered HTTPS URL, including its path and query.
import { randomBytes, createHash } from 'node:crypto';
const verifier = randomBytes(32).toString('base64url');
const state = randomBytes(32).toString('base64url');
const challenge = createHash('sha256').update(verifier).digest('base64url');
// Save { verifier, state } in this user's server-side session.
const query = new URLSearchParams({
response_type: 'code',
client_id: process.env.DOOP_CLIENT_ID,
redirect_uri: 'https://example.com/oauth/doop/callback',
scope: 'profile:read posts:read comments:read',
state,
code_challenge: challenge,
code_challenge_method: 'S256',
});
const authorizationUrl = 'https://doop.live/oauth/authorize?' + query;
// Redirect the user's browser to authorizationUrl.| Authorization parameter | Requirement |
|---|---|
| response_type | Required: code |
| client_id | Required: the UUID shown in Integrations |
| redirect_uri | Required: exact registered HTTPS callback |
| scope | Required: space-separated supported permissions |
| code_challenge | Required: base64url SHA-256 of a 43–128 character verifier |
| code_challenge_method | Required: S256 |
| state | Use an unpredictable value bound to this login attempt; maximum 1,024 characters |
After consent, the callback contains code, your state, and iss=https://doop.live. Verify state and issuer before exchanging the code. A declined request returns error=access_denied instead of a code.
2. Exchange the code
POST /oauth/token accepts form-encoded fields. Codes expire after five minutes and can be used once. Authenticate the client with HTTP Basic as below, or with client_id and client_secret form fields—never both.
curl -X POST "https://doop.live/oauth/token" \
-u "$DOOP_CLIENT_ID:$DOOP_CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$DOOP_CODE" \
--data-urlencode "redirect_uri=https://example.com/oauth/doop/callback" \
--data-urlencode "code_verifier=$DOOP_VERIFIER"{
"access_token": "example_access_token",
"refresh_token": "example_refresh_token",
"token_type": "Bearer",
"expires_in": 900,
"scope": "profile:read posts:read comments:read"
}3. Refresh the connection
Access tokens last 15 minutes; refresh tokens last 30 days. Save both newly returned tokens atomically after every refresh. Serialize refreshes for the same connection; a duplicate refresh fails. Reusing an older refresh token outside the ten-second concurrency window revokes the connection.
curl -X POST "https://doop.live/oauth/token" \
-u "$DOOP_CLIENT_ID:$DOOP_CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "refresh_token=$DOOP_REFRESH_TOKEN"The response has the same shape as the token response above. Omit scope when refreshing; to change permissions, reconnect through consent. The connected account must remain active.
4. Disconnect
curl -X POST "https://doop.live/oauth/revoke" \
-u "$DOOP_CLIENT_ID:$DOOP_CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "token=$DOOP_REFRESH_TOKEN"A successful revocation returns HTTP 200 with {}. Either an access token or refresh token revokes the entire connection. Unknown tokens return the same response. Revocation and Settings disconnection remain available regardless of subscription. Disabling an app or rotating its secret disconnects its access.
OAuth failures
OAuth routes use the OAuth error format rather than the resource API envelope.
{
"error": "invalid_grant",
"error_description": "Credential already used. Reconnect if needed."
}| Status | Error | Resolution |
|---|---|---|
| 400 | invalid_request / unsupported_response_type | Check the registered redirect URL, response_type, and PKCE. |
| 400 | invalid_grant | Check the code, verifier, redirect, refresh token, and expiry. Reconnect if revoked or already used. |
| 400 | invalid_scope / unsupported_grant_type | Use the documented scopes and authorization_code or refresh_token grant. |
| 401 | invalid_client | Check the client ID and secret. Use one authentication method. |
| 403 | ACCOUNT_INACTIVE / APP_NOT_APPROVED / INVALID_CONSENT | Check account status, app approval for this user, and fresh consent. |
| 429 | temporarily_unavailable | Limit token requests; wait before retrying. |
| 503 | API_DISABLED | Wait until API access is available. |
Permissions
Request only what your app needs. A scope never overrides ownership, blocking, moderation, or messaging rules.
| Scope | Access granted |
|---|---|
| profile:read | Read the connected profile and resolve public usernames. |
| posts:read | Read owned Feed and Watch posts. |
| comments:read | Read comments and replies on owned posts. |
| comments:write | Reply to comments on owned posts. |
| conversations:read | List accessible conversations. |
| conversations:write | Find or start a permitted direct conversation. |
| messages:read | Read accessible conversation messages. |
| messages:write | Send text messages within existing messaging rules. |
| webhooks:manage | Create, list, and remove webhook subscriptions. |
New apps may connect only their owner until approved by Doop. All active accounts can connect, regardless of plan. Subscription changes do not interrupt API access or webhooks. Suspended or closed accounts cannot use the API; users can still disconnect apps.
Requests & pagination
| Header | Usage |
|---|---|
| Authorization: Bearer <access_token> | Required on every resource request. |
| Content-Type: application/json | Required for resource JSON POST payloads; keep bodies within 16 KB. |
| Idempotency-Key | Required for every resource POST. Use 8–128 printable non-space characters. |
| X-Request-Id | Returned by the resource handler. Save this when diagnosing a failed request. |
| Cache-Control: no-store | Resource responses must not be cached by shared intermediaries. |
Safe retries
Use one stable idempotency key per intended write. If the connection drops, retry the same method, URL, key, and JSON body. The API returns the stored result without repeating the write. Reusing the key with different data returns 409. Keys belong to an OAuth connection and are not shared across reconnects. DELETE subscription requests are already idempotent and need no key.
List responses
Paginated endpoints return data and next_cursor. Default limit is 25; maximum is 100. Results are ordered by creation time, newest first. Pass a non-null cursor unchanged to fetch the next page. Keep separate cursors for each resource and Feed/Watch type. Webhook listing is a bounded list without cursor pagination.
{
"data": [
{
"id": "feed_<uuid>",
"body": "Example post",
"created_at": "2026-10-11T10:00:00.000Z",
"type": "feed"
}
],
"next_cursor": "opaque_cursor_returned_by_the_api"
}curl --get "https://doop.live/api/v1/posts" \
-H "Authorization: Bearer $DOOP_ACCESS_TOKEN" \
--data-urlencode "type=feed" \
--data-urlencode "limit=25" \
--data-urlencode "cursor=$DOOP_NEXT_CURSOR"For historical synchronization, walk pages until next_cursor is null. For incremental polling, deduplicate stable IDs and walk from the newest page until the last known record. New records created while paging are picked up by the next poll.
Identifiers & returned fields
Post and comment IDs are prefixed with feed_ or watch_. User, conversation, message, and subscription IDs are UUIDs. Dates are ISO 8601 strings. Nullable fields, such as parent_id and closed_at, use JSON null. A root comment has parent_id: null; a reply points to its root. Never infer ownership from an ID.
Errors & rate limits
Resource errors return an error object containing code, message, and request_id. Do not branch on human-readable message text; use status and code.
{
"error": {
"code": "ACCOUNT_INACTIVE",
"message": "An active Doop account is required.",
"request_id": "cb5a1325-ff88-4453-8cd2-aa8fc4127eaf"
}
}| Status | Code | What to do |
|---|---|---|
| 400 | INVALID_JSON / INVALID_BODY | Check the JSON object, required fields, text length, and path IDs. |
| 400 | INVALID_CURSOR / INVALID_LIMIT | Pass the cursor unchanged and use a page size from 1 to 100. |
| 400 | IDEMPOTENCY_KEY_REQUIRED | Supply a printable, non-space key of 8–128 characters on resource POST requests. |
| 400 | INVALID_WEBHOOK_URL / INVALID_URL | Use an eligible public HTTPS destination. |
| 401 | invalid_token | Refresh an expired access token once, then retry. Reconnect if the grant was revoked. |
| 403 | ACCOUNT_INACTIVE | The connected Doop account must be active. |
| 403 | INSUFFICIENT_SCOPE | Reconnect with explicit consent for the required permissions. |
| 403 / 409 | REQUEST_REJECTED | An existing Doop rule rejected the action: blocking, message requests, safety, or a closed conversation. Read the message; do not retry blindly. |
| 404 | NOT_FOUND | The resource is missing, deleted, hidden, or not accessible to this account. |
| 409 | IDEMPOTENCY_CONFLICT | The same key was reused with a different request body. Retry the original body or use a new key for a new action. |
| 409 | LIMIT_REACHED | Remove an unused subscription before adding more than ten active subscriptions. |
| 429 | RATE_LIMITED | Wait at least Retry-After seconds. Queue writes and use backoff with jitter. |
| 500 | INTERNAL_ERROR | Retry with backoff and the same idempotency key. Keep the request ID for support. |
| 503 | API_DISABLED / WEBHOOKS_DISABLED / CONFIGURATION_REQUIRED | The service is not available. Wait for availability rather than reconnecting repeatedly. |
Request allowances
The API allows 120 requests per minute per account and per app/account pair, plus 30 resource POST attempts per hour per account. Existing messaging limits also apply. Retries consume request allowance. The token endpoint allows 60 requests per minute per client.
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 119
X-RateLimit-Reset: 1791712860
X-Request-Id: cb5a1325-ff88-4453-8cd2-aa8fc4127eaf
Cache-Control: no-storeThe reset value is Unix seconds. For a 429, honor Retry-After in seconds; the hourly write cap can differ from the per-minute allowance shown in X-RateLimit headers. Network-level protection may also reject a request before it reaches the resource handler.
Account
Get your account
Identify the account that authorized this connection. Only the public ID, username, and display name are returned.
profile:readNo request parameters or body.
curl "https://doop.live/api/v1/me" \
-H "Authorization: Bearer $DOOP_ACCESS_TOKEN"{
"data": {
"id": "9cae713a-e731-40be-8c77-53bdfab2fd60",
"username": "amara",
"display_name": "Amara Mensah"
}
}Response fields
| Field | Type | Description |
|---|---|---|
| data | object | Returned resource, list, or event payload. |
| data.id | string | Resource identifier. Pass it unchanged to the corresponding endpoint. |
| data.username | string | Public username, without @. |
| data.display_name | string | Public display name. |
Example failure · 401
{
"error": {
"code": "invalid_token",
"message": "Access token is invalid or expired.",
"request_id": "cb5a1325-ff88-4453-8cd2-aa8fc4127eaf"
}
}Posts & comments
Read and reply on your own Feed and Watch content. The same routes support both types through their prefixed IDs.
List your posts
List the connected account’s visible Feed posts or approved native Watch videos, newest first.
posts:readRequest fields
| Field | Type | Required | Description |
|---|---|---|---|
| type | query · feed | watch | No | Defaults to feed. Use watch for native Watch posts. |
| limit | query · integer | No | Items per page: 1–100. Default 25. |
| cursor | query · string | No | The opaque next_cursor from the previous response. Omit on the first request. |
curl "https://doop.live/api/v1/posts?type=feed&limit=25" \
-H "Authorization: Bearer $DOOP_ACCESS_TOKEN"{
"data": [
{
"id": "feed_0ebecbdb-d0a7-473a-b610-58c03a40f983",
"body": "What would make your workflow easier?",
"created_at": "2026-10-11T10:00:00.000Z",
"type": "feed"
}
],
"next_cursor": null
}Response fields
| Field | Type | Description |
|---|---|---|
| data | array | Returned resource, list, or event payload. |
| data[].id | string | Resource identifier. Pass it unchanged to the corresponding endpoint. |
| data[].body | string | Post, comment, reply, or text message content. |
| data[].created_at | string | Creation time in ISO 8601 UTC format. |
| data[].type | string | Feed/Watch post type, or the webhook event name for an event envelope. |
| next_cursor | string | null | Opaque cursor for the next page; null at the end. Pass it unchanged. |
Example failure · 400
{
"error": {
"code": "INVALID_TYPE",
"message": "Choose feed or watch.",
"request_id": "cb5a1325-ff88-4453-8cd2-aa8fc4127eaf"
}
}Feed and Watch have separate cursors. This does not list another user’s posts or publish new content.
Get an owned post
Retrieve a visible post owned by the connected account.
posts:readRequest fields
| Field | Type | Required | Description |
|---|---|---|---|
| id | path · string | Yes | A prefixed owned post ID: feed_<uuid> or watch_<uuid>. |
curl "https://doop.live/api/v1/posts/feed_0ebecbdb-d0a7-473a-b610-58c03a40f983" \
-H "Authorization: Bearer $DOOP_ACCESS_TOKEN"{
"data": {
"id": "feed_0ebecbdb-d0a7-473a-b610-58c03a40f983",
"body": "What would make your workflow easier?",
"created_at": "2026-10-11T10:00:00.000Z",
"type": "feed"
}
}Response fields
| Field | Type | Description |
|---|---|---|
| data | object | Returned resource, list, or event payload. |
| data.id | string | Resource identifier. Pass it unchanged to the corresponding endpoint. |
| data.body | string | Post, comment, reply, or text message content. |
| data.created_at | string | Creation time in ISO 8601 UTC format. |
| data.type | string | Feed/Watch post type, or the webhook event name for an event envelope. |
Example failure · 404
{
"error": {
"code": "NOT_FOUND",
"message": "Resource is not available.",
"request_id": "cb5a1325-ff88-4453-8cd2-aa8fc4127eaf"
}
}Watch responses use type: watch and a watch_ ID; body contains the video caption. Media URLs and uploads are outside v1.
Read post comments
Read top-level comments on one of your Feed or Watch posts. Replies are retrieved separately.
comments:readRequest fields
| Field | Type | Required | Description |
|---|---|---|---|
| id | path · string | Yes | A prefixed owned post ID: feed_<uuid> or watch_<uuid>. |
| limit | query · integer | No | Items per page: 1–100. Default 25. |
| cursor | query · string | No | The opaque next_cursor from the previous response. Omit on the first request. |
curl "https://doop.live/api/v1/posts/feed_0ebecbdb-d0a7-473a-b610-58c03a40f983/comments?limit=25" \
-H "Authorization: Bearer $DOOP_ACCESS_TOKEN"{
"data": [
{
"id": "feed_7973721b-a18c-4da5-90f5-bad85016ce00",
"body": "Can this connect to our support tools?",
"author_user_id": "82ba212a-b26b-4c2e-8edc-d8376638a0c2",
"author_username": "kwame",
"parent_id": null,
"created_at": "2026-10-11T10:00:00.000Z",
"post_id": "feed_0ebecbdb-d0a7-473a-b610-58c03a40f983"
}
],
"next_cursor": null
}Response fields
| Field | Type | Description |
|---|---|---|
| data | array | Returned resource, list, or event payload. |
| data[].id | string | Resource identifier. Pass it unchanged to the corresponding endpoint. |
| data[].body | string | Post, comment, reply, or text message content. |
| data[].author_user_id | string | Comment or reply author's user ID. |
| data[].author_username | string | Comment or reply author's public username. |
| data[].parent_id | string | null | Parent comment ID for a reply; null for a top-level comment. |
| data[].created_at | string | Creation time in ISO 8601 UTC format. |
| data[].post_id | string | Containing owned post ID, including feed_ or watch_. |
| next_cursor | string | null | Opaque cursor for the next page; null at the end. Pass it unchanged. |
Example failure · 404
{
"error": {
"code": "NOT_FOUND",
"message": "Resource is not available.",
"request_id": "cb5a1325-ff88-4453-8cd2-aa8fc4127eaf"
}
}Deleted or moderated comments and comments from blocked contacts are excluded. Other users’ posts return 404.
Read comment replies
Read replies attached to a root comment on your own post.
comments:readRequest fields
| Field | Type | Required | Description |
|---|---|---|---|
| id | path · string | Yes | A prefixed comment ID from an owned post. |
| limit | query · integer | No | Items per page: 1–100. Default 25. |
| cursor | query · string | No | The opaque next_cursor from the previous response. Omit on the first request. |
curl "https://doop.live/api/v1/comments/feed_7973721b-a18c-4da5-90f5-bad85016ce00/replies?limit=25" \
-H "Authorization: Bearer $DOOP_ACCESS_TOKEN"{
"data": [
{
"id": "feed_c3164828-a34c-47a8-9a3b-1d8db3969ec1",
"body": "Yes, you can connect them through the API.",
"author_user_id": "9cae713a-e731-40be-8c77-53bdfab2fd60",
"author_username": "amara",
"parent_id": "feed_7973721b-a18c-4da5-90f5-bad85016ce00",
"created_at": "2026-10-11T10:00:00.000Z",
"post_id": "feed_0ebecbdb-d0a7-473a-b610-58c03a40f983"
}
],
"next_cursor": null
}Response fields
| Field | Type | Description |
|---|---|---|
| data | array | Returned resource, list, or event payload. |
| data[].id | string | Resource identifier. Pass it unchanged to the corresponding endpoint. |
| data[].body | string | Post, comment, reply, or text message content. |
| data[].author_user_id | string | Comment or reply author's user ID. |
| data[].author_username | string | Comment or reply author's public username. |
| data[].parent_id | string | null | Parent comment ID for a reply; null for a top-level comment. |
| data[].created_at | string | Creation time in ISO 8601 UTC format. |
| data[].post_id | string | Containing owned post ID, including feed_ or watch_. |
| next_cursor | string | null | Opaque cursor for the next page; null at the end. Pass it unchanged. |
Example failure · 404
{
"error": {
"code": "NOT_FOUND",
"message": "Resource is not available.",
"request_id": "cb5a1325-ff88-4453-8cd2-aa8fc4127eaf"
}
}Reply to a comment
Reply as the connected account on its own Feed or Watch post.
comments:writeRequest fields
| Field | Type | Required | Description |
|---|---|---|---|
| id | path · string | Yes | A prefixed comment ID from an owned post. |
| body | JSON · string | Yes | Trimmed text, 1–800 characters. |
Include an Idempotency-Key header. Send JSON with Content-Type: application/json.
curl -X POST "https://doop.live/api/v1/comments/feed_7973721b-a18c-4da5-90f5-bad85016ce00/replies" \
-H "Authorization: Bearer $DOOP_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: reply-event-001" \
-d '{"body":"Yes, you can connect them through the API."}'{
"body": "Yes, you can connect them through the API."
}{
"data": {
"id": "feed_c3164828-a34c-47a8-9a3b-1d8db3969ec1",
"body": "Yes, you can connect them through the API.",
"author_user_id": "9cae713a-e731-40be-8c77-53bdfab2fd60",
"author_username": "amara",
"parent_id": "feed_7973721b-a18c-4da5-90f5-bad85016ce00",
"created_at": "2026-10-11T10:00:00.000Z",
"post_id": "feed_0ebecbdb-d0a7-473a-b610-58c03a40f983"
}
}Response fields
| Field | Type | Description |
|---|---|---|
| data | object | Returned resource, list, or event payload. |
| data.id | string | Resource identifier. Pass it unchanged to the corresponding endpoint. |
| data.body | string | Post, comment, reply, or text message content. |
| data.author_user_id | string | Comment or reply author's user ID. |
| data.author_username | string | Comment or reply author's public username. |
| data.parent_id | string | null | Parent comment ID for a reply; null for a top-level comment. |
| data.created_at | string | Creation time in ISO 8601 UTC format. |
| data.post_id | string | Containing owned post ID, including feed_ or watch_. |
Example failure · 409
{
"error": {
"code": "REQUEST_REJECTED",
"message": "Comments are disabled for this video.",
"request_id": "cb5a1325-ff88-4453-8cd2-aa8fc4127eaf"
}
}Replying to a reply attaches your reply to the root thread. Existing commenting and safety restrictions apply.
Conversations & messages
To contact someone new, find their public user ID, create or find a conversation, then send a message. A conversation can exist before a message request is accepted.
Find a recipient
Resolve a public username before starting a conversation. No email or phone search is available.
profile:readRequest fields
| Field | Type | Required | Description |
|---|---|---|---|
| username | path · string | Yes | Username without @. Case-insensitive. URL-encode the path value. |
curl "https://doop.live/api/v1/users/by-username/kojo" \
-H "Authorization: Bearer $DOOP_ACCESS_TOKEN"{
"data": {
"id": "82ba212a-b26b-4c2e-8edc-d8376638a0c2",
"username": "kojo",
"display_name": "Kojo Owusu"
}
}Response fields
| Field | Type | Description |
|---|---|---|
| data | object | Returned resource, list, or event payload. |
| data.id | string | Resource identifier. Pass it unchanged to the corresponding endpoint. |
| data.username | string | Public username, without @. |
| data.display_name | string | Public display name. |
Example failure · 404
{
"error": {
"code": "NOT_FOUND",
"message": "Resource is not available.",
"request_id": "cb5a1325-ff88-4453-8cd2-aa8fc4127eaf"
}
}Inactive and blocked users are not returned. The recipient does not need a paid plan.
List conversations
List conversations accessible to the connected account, ordered by creation time, newest first.
conversations:readRequest fields
| Field | Type | Required | Description |
|---|---|---|---|
| limit | query · integer | No | Items per page: 1–100. Default 25. |
| cursor | query · string | No | The opaque next_cursor from the previous response. Omit on the first request. |
curl "https://doop.live/api/v1/conversations?limit=25" \
-H "Authorization: Bearer $DOOP_ACCESS_TOKEN"{
"data": [
{
"id": "f85a71e9-6d88-4fbe-b1bb-d9371044f568",
"context_type": "DIRECT",
"created_at": "2026-10-11T10:00:00.000Z",
"closed_at": null,
"message_request_state": "PENDING",
"other_user_id": "82ba212a-b26b-4c2e-8edc-d8376638a0c2"
}
],
"next_cursor": null
}Response fields
| Field | Type | Description |
|---|---|---|
| data | array | Returned resource, list, or event payload. |
| data[].id | string | Resource identifier. Pass it unchanged to the corresponding endpoint. |
| data[].context_type | string | Conversation context, such as DIRECT. |
| data[].created_at | string | Creation time in ISO 8601 UTC format. |
| data[].closed_at | string | null | Conversation closure time; null while open. |
| data[].message_request_state | string | Current message-request state. Pending requests restrict sending. |
| data[].other_user_id | string | Other participant in the conversation. |
| next_cursor | string | null | Opaque cursor for the next page; null at the end. Pass it unchanged. |
Example failure · 403
{
"error": {
"code": "INSUFFICIENT_SCOPE",
"message": "This connection does not have the required permission.",
"request_id": "cb5a1325-ff88-4453-8cd2-aa8fc4127eaf"
}
}Includes accessible direct and work conversations. A non-null closed_at means read-only. Listing a message request does not mean it has been accepted.
Find or start a conversation
Find an existing eligible direct conversation or create one with a permitted active recipient.
conversations:writeRequest fields
| Field | Type | Required | Description |
|---|---|---|---|
| recipient_id | JSON · UUID | Yes | The public user ID from recipient lookup. Must be another user. |
Include an Idempotency-Key header. Send JSON with Content-Type: application/json.
curl -X POST "https://doop.live/api/v1/conversations" \
-H "Authorization: Bearer $DOOP_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-conversation-event-001" \
-d '{"recipient_id":"82ba212a-b26b-4c2e-8edc-d8376638a0c2"}'{
"recipient_id": "82ba212a-b26b-4c2e-8edc-d8376638a0c2"
}{
"data": {
"id": "f85a71e9-6d88-4fbe-b1bb-d9371044f568"
}
}Response fields
| Field | Type | Description |
|---|---|---|
| data | object | Returned resource, list, or event payload. |
| data.id | string | Resource identifier. Pass it unchanged to the corresponding endpoint. |
Example failure · 403
{
"error": {
"code": "REQUEST_REJECTED",
"message": "Contact is unavailable between these accounts.",
"request_id": "cb5a1325-ff88-4453-8cd2-aa8fc4127eaf"
}
}Concurrent requests reuse the existing direct-conversation rules. Returns 201 with the conversation ID whether found or created. Starting a conversation sends no text message and does not bypass message-request rules.
Read messages
Read visible user messages in a conversation you can access. Internal system messages, card metadata, and attachments are excluded.
messages:readRequest fields
| Field | Type | Required | Description |
|---|---|---|---|
| id | path · UUID | Yes | A conversation in which the connected account is a participant. |
| limit | query · integer | No | Items per page: 1–100. Default 25. |
| cursor | query · string | No | The opaque next_cursor from the previous response. Omit on the first request. |
curl "https://doop.live/api/v1/conversations/f85a71e9-6d88-4fbe-b1bb-d9371044f568/messages?limit=25" \
-H "Authorization: Bearer $DOOP_ACCESS_TOKEN"{
"data": [
{
"id": "b67c0875-60a4-4c29-8bcb-759a5a29ea75",
"conversation_id": "f85a71e9-6d88-4fbe-b1bb-d9371044f568",
"sender_user_id": "9cae713a-e731-40be-8c77-53bdfab2fd60",
"body": "Hello! Thanks for reaching out.",
"created_at": "2026-10-11T10:00:00.000Z"
}
],
"next_cursor": null
}Response fields
| Field | Type | Description |
|---|---|---|
| data | array | Returned resource, list, or event payload. |
| data[].id | string | Resource identifier. Pass it unchanged to the corresponding endpoint. |
| data[].conversation_id | string | Containing conversation ID. |
| data[].sender_user_id | string | Message sender's user ID. |
| data[].body | string | Post, comment, reply, or text message content. |
| data[].created_at | string | Creation time in ISO 8601 UTC format. |
| next_cursor | string | null | Opaque cursor for the next page; null at the end. Pass it unchanged. |
Example failure · 404
{
"error": {
"code": "NOT_FOUND",
"message": "Resource is not available.",
"request_id": "cb5a1325-ff88-4453-8cd2-aa8fc4127eaf"
}
}Send a message
Send a text message as the connected account. Works in existing and newly created permitted conversations.
messages:writeRequest fields
| Field | Type | Required | Description |
|---|---|---|---|
| id | path · UUID | Yes | A conversation in which the connected account is a participant. |
| body | JSON · string | Yes | Trimmed text, 1–1,000 characters. |
Include an Idempotency-Key header. Send JSON with Content-Type: application/json.
curl -X POST "https://doop.live/api/v1/conversations/f85a71e9-6d88-4fbe-b1bb-d9371044f568/messages" \
-H "Authorization: Bearer $DOOP_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: send-message-event-001" \
-d '{"body":"Hello! Thanks for reaching out."}'{
"body": "Hello! Thanks for reaching out."
}{
"data": {
"id": "b67c0875-60a4-4c29-8bcb-759a5a29ea75",
"conversation_id": "f85a71e9-6d88-4fbe-b1bb-d9371044f568",
"sender_user_id": "9cae713a-e731-40be-8c77-53bdfab2fd60",
"body": "Hello! Thanks for reaching out.",
"created_at": "2026-10-11T10:00:00.000Z"
}
}Response fields
| Field | Type | Description |
|---|---|---|
| data | object | Returned resource, list, or event payload. |
| data.id | string | Resource identifier. Pass it unchanged to the corresponding endpoint. |
| data.conversation_id | string | Containing conversation ID. |
| data.sender_user_id | string | Message sender's user ID. |
| data.body | string | Post, comment, reply, or text message content. |
| data.created_at | string | Creation time in ISO 8601 UTC format. |
Example failure · 409
{
"error": {
"code": "REQUEST_REJECTED",
"message": "This conversation is read-only.",
"request_id": "cb5a1325-ff88-4453-8cd2-aa8fc4127eaf"
}
}Message-request restrictions, recipient blocking, and domain sending limits still apply. A second introduction may be rejected until the recipient accepts. Bulk sending and attachments are unsupported.
Webhook endpoints
Retrieve a delivered event
Retrieve authoritative event data using OAuth. For queued consumers such as Zapier, treat a callback only as an untrusted event ID and use this response instead of its payload.
webhooks:manage + event read scopeRequest fields
| Field | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | The stable webhook event ID. |
curl "https://doop.live/api/v1/webhooks/events/122da9c3-2ed9-4702-82dc-5d72b6ac76fb" \
-H "Authorization: Bearer $DOOP_ACCESS_TOKEN"{
"data": {
"id": "122da9c3-2ed9-4702-82dc-5d72b6ac76fb",
"type": "comment.created",
"api_version": "v1",
"created_at": "2026-10-11T10:00:00.000Z",
"originating_app_id": null,
"data": {
"id": "feed_7973721b-a18c-4da5-90f5-bad85016ce00",
"body": "Can this connect to our support tools?",
"author_user_id": "82ba212a-b26b-4c2e-8edc-d8376638a0c2",
"author_username": "kwame",
"parent_id": null,
"created_at": "2026-10-11T10:00:00.000Z",
"post_id": "feed_0ebecbdb-d0a7-473a-b610-58c03a40f983"
}
}
}Response fields
| Field | Type | Description |
|---|---|---|
| data | object | Returned resource, list, or event payload. |
| data.id | string | Event identifier for deduplication; the resource ID is data.data.id. |
| data.type | string | Feed/Watch post type, or the webhook event name for an event envelope. |
| data.api_version | string | Version of the webhook event contract. |
| data.created_at | string | Creation time in ISO 8601 UTC format. |
| data.originating_app_id | string | null | OAuth app that caused the event; null for ordinary Doop activity. |
| data.data | object | Returned resource, list, or event payload. |
| data.data.id | string | Resource identifier. Pass it unchanged to the corresponding endpoint. |
| data.data.body | string | Post, comment, reply, or text message content. |
| data.data.author_user_id | string | Comment or reply author's user ID. |
| data.data.author_username | string | Comment or reply author's public username. |
| data.data.parent_id | string | null | Parent comment ID for a reply; null for a top-level comment. |
| data.data.created_at | string | Creation time in ISO 8601 UTC format. |
| data.data.post_id | string | Containing owned post ID, including feed_ or watch_. |
Example failure · 404
{
"error": {
"code": "NOT_FOUND",
"message": "Webhook event unavailable.",
"request_id": "cb5a1325-ff88-4453-8cd2-aa8fc4127eaf"
}
}Requires an attempted delivery to an active subscription belonging to this exact OAuth connection. Current event scopes, subscription filters, and resource access are rechecked. Events are retained for 30 days; resources reflect their current state. Deduplicate by event ID. This authenticated retrieval is an alternative to trusting the callback body; ordinary receivers should still verify signatures at receipt.
List subscriptions
List subscriptions belonging to this OAuth connection.
webhooks:manageNo request parameters or body.
curl "https://doop.live/api/v1/webhooks" \
-H "Authorization: Bearer $DOOP_ACCESS_TOKEN"{
"data": [
{
"id": "917d36dd-6d59-40b2-93a8-c1fc6d69a31a",
"url": "https://example.com/webhooks/doop",
"events": [
"comment.created",
"comment.replied"
],
"post_filter": "feed_0ebecbdb-d0a7-473a-b610-58c03a40f983",
"ignore_own_app": true,
"created_at": "2026-10-11T10:00:00.000Z",
"disabled_at": null
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
| data | array | Returned resource, list, or event payload. |
| data[].id | string | Resource identifier. Pass it unchanged to the corresponding endpoint. |
| data[].url | string | Public HTTPS webhook destination. |
| data[].events | array | Subscribed event names. |
| data[].post_filter | string | null | Owned post filter; null means all eligible posts. |
| data[].ignore_own_app | boolean | Whether this subscription excludes events created by its own OAuth app. |
| data[].created_at | string | Creation time in ISO 8601 UTC format. |
| data[].disabled_at | string | null | Time the subscription was disabled; null while enabled. |
Example failure · 403
{
"error": {
"code": "INSUFFICIENT_SCOPE",
"message": "This connection does not have the required permission.",
"request_id": "cb5a1325-ff88-4453-8cd2-aa8fc4127eaf"
}
}Returns at most 100 subscriptions, newest first, including disabled subscriptions. No cursor and no signing secrets are returned.
Subscribe to events
Subscribe to new activity on the connected account.
webhooks:manage + event read scopesRequest fields
| Field | Type | Required | Description |
|---|---|---|---|
| url | JSON · HTTPS URL | Yes | Public endpoint, standard HTTPS port, no URL credentials or fragments. Redirects are not followed. |
| events | JSON · string[] | Yes | One or more supported event names. Each needs its corresponding read scope. |
| post_id | JSON · string | null | No | Filter to a specific owned post. Omit or pass null for all posts. Do not use a post filter for message.received. |
| ignore_own_app | JSON · boolean | No | Defaults to true. Excludes events produced by this same app. |
Include an Idempotency-Key header. Send JSON with Content-Type: application/json.
curl -X POST "https://doop.live/api/v1/webhooks" \
-H "Authorization: Bearer $DOOP_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-webhook-event-001" \
-d '{"url":"https://example.com/webhooks/doop","events":["comment.created","comment.replied"],"post_id":"feed_0ebecbdb-d0a7-473a-b610-58c03a40f983","ignore_own_app":true}'{
"url": "https://example.com/webhooks/doop",
"events": [
"comment.created",
"comment.replied"
],
"post_id": "feed_0ebecbdb-d0a7-473a-b610-58c03a40f983",
"ignore_own_app": true
}{
"data": {
"id": "917d36dd-6d59-40b2-93a8-c1fc6d69a31a",
"url": "https://example.com/webhooks/doop",
"events": [
"comment.created",
"comment.replied"
],
"post_filter": "feed_0ebecbdb-d0a7-473a-b610-58c03a40f983",
"ignore_own_app": true,
"created_at": "2026-10-11T10:00:00.000Z",
"signing_secret": "example_only_save_the_real_secret_securely"
}
}Response fields
| Field | Type | Description |
|---|---|---|
| data | object | Returned resource, list, or event payload. |
| data.id | string | Resource identifier. Pass it unchanged to the corresponding endpoint. |
| data.url | string | Public HTTPS webhook destination. |
| data.events | array | Subscribed event names. |
| data.post_filter | string | null | Owned post filter; null means all eligible posts. |
| data.ignore_own_app | boolean | Whether this subscription excludes events created by its own OAuth app. |
| data.created_at | string | Creation time in ISO 8601 UTC format. |
| data.signing_secret | string | Webhook signing secret, returned once at creation. Store it securely. |
Example failure · 400
{
"error": {
"code": "INVALID_WEBHOOK_URL",
"message": "Webhook must resolve only to public addresses.",
"request_id": "cb5a1325-ff88-4453-8cd2-aa8fc4127eaf"
}
}Save the signing_secret from this response; it is not included in subscription lists. Maximum ten active subscriptions per connection. Historical records are available through list endpoints.
Remove a subscription
Disable a subscription owned by this connection. Repeating the request is safe.
webhooks:manageRequest fields
| Field | Type | Required | Description |
|---|---|---|---|
| id | path · UUID | Yes | The subscription ID returned at creation. |
curl -X DELETE "https://doop.live/api/v1/webhooks/917d36dd-6d59-40b2-93a8-c1fc6d69a31a" \
-H "Authorization: Bearer $DOOP_ACCESS_TOKEN"{
"deleted": true
}Response fields
| Field | Type | Description |
|---|---|---|
| deleted | boolean | Whether the subscription was removed. |
Example failure · 401
{
"error": {
"code": "invalid_token",
"message": "Access token is invalid or expired.",
"request_id": "cb5a1325-ff88-4453-8cd2-aa8fc4127eaf"
}
}Requires a valid access token. Available regardless of subscription. Use Settings → Integrations to disconnect if your access token has expired. A subscription not owned by this connection is left untouched.
Receiving webhooks
| Event | Read scope | When it occurs |
|---|---|---|
| comment.created | comments:read | A new top-level comment is added to an owned post. |
| comment.replied | comments:read | A reply is added to a comment on an owned post. |
| message.received | messages:read | An incoming user message is added to an accessible conversation. |
| post.published | posts:read | A Feed post is created or a native Watch post becomes approved. |
Subscriptions start with new activity. They do not send historical data. Events come from both normal app use and API activity. The default ignore_own_app filter prevents your own integration’s writes from triggering a loop. Message events exclude system messages and your own outgoing messages.
Content-Type: application/json
X-Doop-Event-Id: 1ccfe516-b6a2-46cf-a932-18c57ee8e2f1
X-Doop-Signature: t=1791712800,v1=<hex_hmac_sha256>{
"id": "1ccfe516-b6a2-46cf-a932-18c57ee8e2f1",
"type": "comment.created",
"api_version": "v1",
"created_at": "2026-10-11T10:00:00.000Z",
"originating_app_id": null,
"data": {
"id": "feed_7973721b-a18c-4da5-90f5-bad85016ce00",
"body": "Can this connect to our support tools?",
"author_user_id": "82ba212a-b26b-4c2e-8edc-d8376638a0c2",
"author_username": "kwame",
"parent_id": null,
"created_at": "2026-10-11T10:00:00.000Z",
"post_id": "feed_0ebecbdb-d0a7-473a-b610-58c03a40f983"
}
}data is a single comment, message, or post with the same fields as the corresponding resource response. originating_app_id is null for normal app activity, or the app UUID for API-generated activity. Payload data is read at delivery time; deleted, blocked, or inaccessible content is withheld.
Verify the exact raw body
Compute HMAC-SHA256 over the timestamp, a period, and the unmodified raw request body using your subscription’s signing secret. Compare in constant time and reject timestamps more than five minutes from your server clock. Do not parse and reserialize the body before verification.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyDoopWebhook(rawBody, signature, secret) {
const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(signature || '');
if (!match || Math.abs(Date.now() / 1000 - Number(match[1])) > 300) {
throw new Error('Invalid or expired signature');
}
const expected = createHmac('sha256', secret)
.update(match[1] + '.').update(rawBody).digest();
const supplied = Buffer.from(match[2], 'hex');
if (supplied.length !== expected.length || !timingSafeEqual(expected, supplied)) {
throw new Error('Invalid signature');
}
return JSON.parse(rawBody.toString('utf8'));
}
// Capture a Buffer from your HTTP framework before its JSON parser.
// Verify, durably deduplicate event.id, enqueue the work, then return 2xx.Delivery guarantees
Delivery is at least once and may arrive out of order. Event IDs remain stable across retries. Persist IDs to prevent duplicate downstream actions. Acknowledge with any 2xx response only after durable acceptance. Non-2xx responses and timeouts trigger retries; redirects are not followed.
Retry delays after failed attempts are 30 seconds, 2 minutes, 10 minutes, 30 minutes, 2 hours, 6 hours, and 12 hours. Delivery stops after eight attempts or seven days. Each HTTP attempt times out after ten seconds. Timing can be later depending on worker scheduling.
Inspect delivery status and attempt history in Settings → Integrations → Webhooks. Eligible failed or skipped events within seven days can be explicitly replayed with the same event ID. Diagnostics are retained for 30 days. Expiry, disconnection, or lost permission stops delivery; upgrading does not replay missed events automatically.
Webhook URLs must resolve only to public HTTPS destinations without URL credentials, fragments, or custom ports. Private, loopback, metadata, and reserved addresses are rejected. DNS is rechecked for each attempt.
Automation recipes
Comment → spreadsheet
- Connect with comments:read and webhooks:manage. Add posts:read if you need to discover post IDs.
- Create a comment.created subscription, optionally filtered to one owned post.
- Verify the signature and deduplicate the event ID.
- Append data.id, data.post_id, data.author_user_id, data.body, and data.created_at to your spreadsheet.
Incoming message → support workflow
- Connect with messages:read and webhooks:manage.
- Subscribe to message.received and store the conversation_id with your support ticket.
- If your workflow sends a response, also request messages:write and use a stable per-action idempotency key.
Automation → new conversation
- Request profile:read, conversations:write, and messages:write.
- Look up the recipient by username and use their returned ID to create or find a direct conversation.
- Send the first message using the returned conversation ID. Handle a pending message request instead of repeatedly sending.
const response = await fetch(
'https://doop.live/api/v1/conversations/' + conversationId + '/messages',
{
method: 'POST',
headers: {
Authorization: 'Bearer ' + accessToken,
'Content-Type': 'application/json',
'Idempotency-Key': sourceEventId + ':support-reply',
},
body: JSON.stringify({ body: 'Thanks! Our team will follow up here.' }),
},
);
const result = await response.json();
if (!response.ok) {
// Honor Retry-After on 429. Retry transient failures with the same key.
throw new Error(result.error?.code || 'Request failed');
}
console.log(result.data.id);OAuth and webhook contracts can be used by your own backend or automation platform. The Zapier connector is available by invitation while publication and reviewer access are being arranged. The Make app has been submitted for review and is pending approval. Neither submission nor successful test runs mean a public directory listing is available.
Ready to connect?
Create an app and put your first workflow together.
Make integration
During review, use the Doop app invitation, add a Doop module, and create a connection. Sign in with Google, GitHub, or LinkedIn, then approve the requested permissions. A paid Doop subscription is not required. The official Make OAuth client is approved for consenting accounts beyond its owner; custom clients still require separate Doop approval. Revoke access in Settings → Integrations.
Triggers and actions
Triggers include Watch comments, Watch replies, Watch received messages, Watch published posts, and Watch activity. The combined trigger needs filters on Event type before using event-specific fields. Posts & comments contains Get a post, List posts, List comments, List replies, and Create a reply. Conversations & messages contains List conversations, List messages, Create a conversation, and Send a message. Account & users contains Get my profile and Find a user. Other contains Make an API call.
Map Resource ID from a trigger into the matching resource action. Event ID identifies the notification and is used for deduplication. Create a conversation returns only Conversation ID; List conversations provides additional conversation details. Ownership, blocking, and message-request rules apply to every action.
Post pickers and list limits
Get a post, List comments, and comment/reply webhook filters offer Feed or Watch followed by a post picker. The picker follows pages of 100 up to 500 posts. Map a prefixed Post ID for a post outside that list. Get a post starts in mapping mode. The type selector only affects picker options; leaving an optional webhook Post ID blank watches all Feed and Watch posts. The connector omits the empty filter; the API also accepts null as no filter.
List modules default to 10 results. Limit is optional: leave it empty for all available records. Requests fetch 100 records per page, following next_cursor, and Make trims to the chosen total. Large unbounded lists can consume more time and credits.
Prevent duplicate replies and messages
Map Event ID plus a distinct step name into Unique operation key, for example Event ID followed by :reply or:message. Retain the same key when retrying the same operation. A new operation needs a new source-event key. A constant reused across runs returns the previous result without another write; changing the input under that key returns HTTP 409. Random defaults are intentionally avoided because they can duplicate writes on retry. For other workflows, persist a source task identifier plus a step name.
API calls and troubleshooting
Make an API call takes a path such as api/v1/me, query parameters, optional headers, and a JSON body. Authorization comes from the connection; POST requests need a Unique operation key. OAuth failures show error and error_description. Resource failures show error.code and error.message, with a response-body fallback for network-level failures. Reconnect after revoked access, check ownership and blocking for 403/404, and retain the operation key when retrying temporary failures. See Errors & rate limits above.
Built-in automations
Open Profile → Account settings → Automations to build a sequence inside Doop. All active accounts can create automations. Free includes 1 automation with 2 steps; Creator ($5/month) includes 3 automations with 4 steps each; Creator Plus ($20/month) includes unlimited automations and steps. Every card counts, including the trigger and optional keyword condition.
| Setting | Options and behavior |
|---|---|
| Automation name | A name of up to 80 characters. |
| Trigger | New comment, new reply, or incoming message. |
| Post scope | All Feed and Watch posts, one post type, or a selected owned post. Message triggers use the incoming conversation. |
| Keyword condition | Any text, a whole word or phrase, contains text, or an exact match. Matching ignores capitalization. Up to ten comma-separated terms; any one match is enough. |
| Actions | Reply to the triggering comment (up to 800 characters), send its author a message, or reply in the triggering conversation (up to 1,000 characters). Add actions in sequence within your plan’s step allowance. |
| Canvas | Drag cards by their grip, connect or disconnect consecutive steps, and zoom. Moving a card changes its position; Move earlier/later changes action order. Reconnect every step before saving. |
| Enabled / Disabled | Use the header toggle to enable the sequence on save or save it paused. Earlier activity is not replayed. |
| Run history | The latest 50 runs show pending, succeeded, skipped, or failed activity. Refresh to load recent results. |
All plans retain a safety limit of 30 completed sequences per hour per account. Your own comments and automated/API activity are ignored. Blocking, ownership, and message-request rules still apply; incoming message requests must be accepted before an automatic reply can be sent. If any action fails, the sequence rolls back together. Temporary failures retry up to three attempts.
Pause, edit, or delete an automation in the manager. Changes discard queued work from the previous version. After a downgrade, the oldest saved automations retain the available plan slots. Automations over the count or step allowance stay saved but cannot run until they fit the plan. Paused automations also occupy a slot. Restoring access does not replay missed activity.

