A digital twin is a stateful clone of an external service (Stripe, Slack, GitHub, Dropbox, Google Drive, Google Calendar, Box, Notion, Discord, and more) that replicates the endpoints, webhooks, edge cases, and error modes of the real thing. Twins are how Arga gives every PR a sandbox that behaves like production without touching real APIs.Documentation Index
Fetch the complete documentation index at: https://docs.argalabs.com/llms.txt
Use this file to discover all available pages before exploring further.
Why digital twins?
Real integrations evolve. Static mocks don’t, so tests drift from production. And when staging hits real Stripe, Slack, or Notion, state leaks across runs and you can’t simulate failures, rate limits, or webhook errors. Digital twins solve both problems.| Approach | Stateful | Behavioural | Edge cases | Safe |
|---|---|---|---|---|
| Real service | Yes | Yes | Yes | No |
| Static mocks | No | No | No | Yes |
| Digital twins | Yes | Yes | Yes | Yes |
Properties
Stateful
Stateful
The twin remembers state before an API call and updates state after the call completes. If you create a Stripe customer, subsequent calls reflect that customer’s existence — just like the real Stripe API.
Behavioural
Behavioural
The twin reacts the same way the real service would to an API call, including error responses, rate limits, and side effects. A payment to a non-existent customer returns the same error code Stripe would.
Supported services
Arga currently supports these sandbox twins. Twins are split into two categories:- Frontend twins expose a browsable sandbox surface in the Arga UI so you can interact with the twin directly during a run — useful for services with meaningful user-facing state (chats, files, dashboards, checkout pages).
- Backend twins intercept outbound API traffic only. They have no UI surface because the underlying service is consumed purely programmatically.
Frontend twins
| Twin | What it stands in for | Notes |
|---|---|---|
discord | Discord API v10 (discord.com, api.discord.com, discordapp.com) | Intercepts Discord API v10 operations including guilds, channels, messages, reactions, threads, members, roles, bans, invites, webhooks, emojis, stickers, scheduled events, stage instances, auto-moderation rules, soundboard sounds, guild templates, and user endpoints. The Create Message endpoint (POST /channels/{channel_id}/messages) accepts multipart/form-data requests with file attachments — uploaded files are recorded as attachment objects on the message with id, filename, size, url, proxy_url, and content_type fields, matching the real Discord API response shape. You can send message content via a payload_json field or a plain content form field alongside one or more files[n] (or file) parts. Supports deterministic seeding, bot-token authentication, configurable server boost levels (controlling file upload size and emoji slot limits), and event delivery with webhook subscriptions. |
dropbox | Dropbox API v2 (api.dropboxapi.com, content.dropboxapi.com, notify.dropboxapi.com) | Intercepts Dropbox API v2 operations including files (list, upload, download, copy, move, delete, search, revisions, tags), sharing (shared links, shared folders, file members), file requests, file properties, team management (members, groups, team folders, legal holds, namespaces, devices, linked apps, sharing allowlist, audit log), and admin controls. Supports upload sessions, batch operations, webhook deliveries, and event replay. Seeded with a starter folder tree and team members for deterministic testing. Enforces account tier-based storage quotas — uploads that exceed the quota return an insufficient_space error. |
github | GitHub API (api.github.com, github.com) | Intercepts GitHub REST API operations including repositories, file contents (branch-aware), pull requests (with merge and reviewer requests), issues, branches (with protection rules), commits, check runs, check suites, git references, labels (repo-level and issue-level), pull request reviews and review comments, commit statuses, search, organizations, users, GitHub App endpoints (installations, access tokens, repository listings), and webhooks. Supports deterministic seeding (repos, files, issues, PRs, and GitHub App configuration), token authentication with OAuth scope enforcement (parent scopes automatically grant children), configurable default token scopes, OAuth flows, and webhook delivery with signed payloads for push, pull request, check run, and check suite events. |
google_calendar | Google Calendar API (www.googleapis.com, calendar.googleapis.com) | Intercepts Google Calendar v3 operations including calendars, calendar list, events, ACL rules, settings, free/busy queries, colors, and webhook deliveries (with watch support for events, ACL, calendar list, and settings resources). |
google_drive | Google Drive API (www.googleapis.com, content.googleapis.com) | Intercepts Google Drive v3 operations including files (list, get, create, update, copy, delete, export, watch), permissions, revisions, comments, replies, shared drives, change feeds, and resumable uploads. Serves a discovery document compatible with the official Google API client libraries. File resources include the full set of common fields (capabilities, links, ownership, thumbnails) expected by the Google Drive SDK. Enforces OAuth scope requirements on every API call with hierarchical scope expansion (for example, drive implies drive.readonly, drive.file, and drive.metadata). Enforces storage tier-based quotas — uploads that exceed the quota return a storageQuotaExceeded error. |
notion | Notion API (api.notion.com, notion.so) | Intercepts Notion API operations for pages, databases, blocks, users, and search. Enforces workspace plan-based limits on file uploads and guest counts. Supports capability-based access control on user endpoints with granular email visibility (read_user_information_with_email, read_user_information_no_email, no_user_information). |
slack | Slack Web API and OAuth | Intercepts Slack Web API operations including channels, messages, users, conversations, reactions, and files (upload, external upload, info, list, delete). Supports conversations.open for opening or creating direct-message channels. Completing an external file upload via files.completeUploadExternal automatically creates a channel message with the uploaded files attached (including a file_share subtype event), matching real Slack behavior. File uploads enforce tier-based size limits (see tier limits below) — all tiers default to a 1 GB cap, and you can switch tiers via the admin API to test tier-specific behavior. The twin also supports workspace-level constraints for disabling file uploads entirely, enforcing storage quotas, and simulating rate limits. Uploaded files are downloadable from /files/{file_id}. Preserves the Authorization header so you can test token-scoped behavior, including xoxe.-prefixed enterprise tokens. Enforces OAuth scope requirements on every authenticated API call — tokens missing the required scope for a method receive a missing_scope error (see configurable token scopes). Supports the OAuth 2.0 authorization code grant flow (/oauth/v2/authorize and /api/oauth.v2.access), allowing you to test Slack “Add to Slack” install flows end-to-end without touching the real Slack OAuth servers. Bot and user token scopes are configurable via scenario seeding. |
stripe | Stripe API (api.stripe.com, files.stripe.com, connect.stripe.com) | Intercepts Stripe API v1 operations including customers, payment methods, payment intents, setup intents, charges, refunds, disputes, subscriptions, invoices, invoice items, credit notes, products, prices, plans, coupons, promotion codes, tax rates, shipping rates, tokens, sources, payouts, balance, balance transactions, billing meters, meter events, meter event summaries, events, files, file links, checkout sessions, payment links, quotes, billing portal sessions, subscription items, subscription schedules, usage records, customer balance transactions, tax IDs, webhook endpoints, mandates, and test clocks. Supports idempotency keys, Stripe-compatible webhook delivery with signed payloads, test card numbers for simulating declines and 3D Secure flows, and browser-facing pages for checkout, a multi-page dashboard (home, payments, subscriptions, invoices, balances), product catalog, and customer management. |
Backend twins
| Twin | What it stands in for | Notes |
|---|---|---|
box | Box API (api.box.com, upload.box.com, app.box.com) | Intercepts Box v2.0 operations including files, folders, users, search, events, and webhooks. Supports deterministic seeding and OAuth flows. |
jira | Jira Cloud REST API v3 (*.atlassian.net) | Intercepts Jira Cloud REST API v3 operations including issues (create, get, update, delete, bulk create, transitions, changelog, edit metadata), projects, comments, worklogs, attachments, issue links, issue properties, votes, watchers, search and JQL search (/rest/api/3/search, /rest/api/3/search/jql, /rest/api/3/search/count), users (myself, search, bulk, assignable, view-issue), components, versions, remote links, filters, dashboards, groups (and group members), fields, priorities, resolutions, statuses, status categories, issue types, server info, attachments metadata, and webhooks (/rest/api/3/webhook). Includes Agile (Jira Software) endpoints under /rest/agile/1.0/ for boards, sprints, board sprints, and backlog issue moves. Routes are also reachable via the /rest/api/2/ and /rest/api/latest/ aliases. Comments and descriptions are stored as Atlassian Document Format (ADF) — plain strings sent on input are auto-wrapped in a minimal ADF document. Supports the OAuth 2.0 authorization code grant flow with token issuance, scope enforcement, JQL search, deterministic seeding, and webhook delivery with HMAC-signed payloads. |
unified | Unified API (api.unified.to) | Use this only when your app talks to Unified.to rather than directly to an upstream provider. Seeds integration connections that proxy to Slack, Google Mail, Google Drive, Google Calendar, Box, Notion, and Dropbox. For actual provider data (channels, files, pages, repos, etc.), use the provider-specific twin instead. |
unstructured | Unstructured API (api.unstructuredapp.io, platform.unstructuredapp.io) | Intercepts Unstructured API operations including document partitioning, workflow management, source and destination CRUD, and job execution. Supports deterministic seeding and API key authentication. |
- Use integrations to connect context sources that help Arga understand your stack.
- Use digital twins to intercept outbound traffic during sandbox validation.
Stub alerting
Not every endpoint in a digital twin has a full stateful implementation. Endpoints that are not yet statefully implemented return stub responses — schema-valid mock data generated from the service’s API specification. Stub responses are clearly marked so you can distinguish them from real stateful behavior:X-Twin-Stubheader — set to"true"on every stub response._twin_stubfield — a boolean (true) injected into the JSON response body._twin_warningfield — a human-readable string explaining which endpoint is stubbed.
_twin_stub, _twin_warning, and a data key containing the original array.
X-Twin-Stub header to programmatically detect stub responses in your test suite and fail tests that rely on stubbed data.
Tracking stub hits
The GitHub twin exposes an admin endpoint to audit which stubbed endpoints were called during a session:| Field | Type | Description |
|---|---|---|
total_hits | integer | Total number of stub endpoint calls since the last clear. |
unique_endpoints | integer | Number of distinct stubbed endpoints called. |
summary | array | Deduplicated list of endpoints with hit counts, sorted by most-called first. |
summary[].endpoint | string | HTTP method and path template of the stubbed endpoint. |
summary[].hits | integer | Number of times this endpoint was called. |
recent | array | The 20 most recent stub hits with full details. |
{"ok": true} on success.
How Arga uses digital twins
When Arga spins up a sandbox for a PR, it forks only the services the PR touches and routes all their external API calls through the appropriate digital twin. The rest of your stack stays on your production services. This means:- No real side effects — A PR that sends a Slack notification or charges a Stripe card won’t do either during testing.
- Deterministic results — Tests produce consistent outcomes regardless of external service availability.
- Edge case coverage — Twins can simulate failure modes (timeouts, rate limits, malformed responses) that are hard to trigger against real services.
Authentication handling
When requests are routed through a digital twin, Arga controls which headers are forwarded to the twin. For most twins, theAuthorization header is stripped from proxied requests because the twin doesn’t need real credentials — it simulates the service regardless of auth tokens.
The Slack twin is an exception. Slack API calls include the Authorization header when forwarded to the twin, allowing the twin to validate token-scoped behaviour such as bot-token versus user-token permission differences. This means your sandbox tests exercise the same authentication paths your code uses in production, without touching the real Slack API.
The Slack twin enforces OAuth scope requirements on every authenticated API call. Each Slack Web API method requires a specific scope (for example, chat.postMessage requires chat:write, conversations.list requires channels:read). If a token does not include the required scope, the twin returns a missing_scope error — matching real Slack behaviour:
* bypass scope checks and are accepted for any method. You can selectively disable scopes using the disabled_scopes config field — any scope in the disabled list is removed from the effective scope set before enforcement, even if the token originally had it.
The Slack twin also supports the full OAuth 2.0 authorization code grant flow. When SLACK_TWIN_BASE_URL is set, Arga routes OAuth requests (/oauth/v2/authorize and /api/oauth.v2.access) through the twin so you can test “Add to Slack” install flows, token exchange, and scope negotiation in a sandbox. Issued tokens (xoxb- for bots, xoxp- for users, and xoxe.-prefixed enterprise tokens) are fully functional within the twin and can be used for subsequent API calls like auth.test and search.messages. The scopes granted to tokens issued via the OAuth flow are determined by the twin’s configurable oauth_bot_scopes and oauth_user_scopes settings (see configurable token scopes below).
The GitHub twin preserves the Authorization header and enforces OAuth scope requirements on API calls. Each API route requires a specific scope (for example, POST /repos/{owner}/{repo}/issues requires repo, GET /user requires read:user). Parent scopes automatically grant their children — repo implies repo:status, repo_deployment, repo:invite, public_repo, and security_events; admin:repo_hook implies write:repo_hook and read:repo_hook; and so on. If a token does not include a required scope, the twin returns a 403 matching GitHub’s real API response shape:
default_token_scopes config field. Scopes can also be selectively disabled using the disabled_scopes config field — any scope in the disabled list is removed from the effective scope set, even if the token originally had it.
The Google Drive twin also enforces OAuth scope requirements on every API call. Each Drive API operation requires at least one of a set of accepted scopes (for example, files.create requires drive or drive.file or drive.appdata). Parent scopes automatically grant their children — drive implies drive.readonly, drive.file, drive.appdata, drive.metadata, and drive.scripts. If a token lacks any accepted scope for an operation, the twin returns a 403 insufficientPermissions error matching the real Google Drive API. You can selectively disable scopes using the disabled_scopes config field.
The Notion twin enforces capability-based access on user info endpoints. The twin recognizes three user-information capability levels: read_user_information_with_email (full access including email), read_user_information_no_email (user info without email), and no_user_information (no access). The legacy read_user_information capability is treated as equivalent to read_user_information_with_email. If a token has no user info capability, user endpoints (/v1/users, /v1/users/me, /v1/users/{user_id}) return a 403 restricted_resource error. You can selectively disable capabilities using the disabled_capabilities config field.
The Stripe twin also preserves the Authorization header. The twin validates that API keys use a recognized prefix (sk_test_, sk_live_, rk_test_, or rk_live_) and returns Stripe-compatible authentication errors for missing or invalid keys. This means your code’s Stripe authentication logic runs the same way in the sandbox as it does in production.
The Jira twin preserves the Authorization header and requires every non-public request to use a Bearer token. Requests missing or with a malformed Authorization header receive a 401 matching Jira’s real API response shape:
POST /rest/api/3/issue requires write:jira-work, GET /rest/api/3/myself requires read:jira-user, and POST /rest/api/3/component requires manage:jira-project. Parent scopes automatically grant their children: write:jira-work implies read:jira-work, manage:jira-project implies read:jira-work, and manage:jira-configuration implies read:jira-work and manage:jira-project. Tokens that lack a required scope receive a 403 with a missing-scope message:
read:jira-work, write:jira-work, read:jira-user, manage:jira-project, manage:jira-webhook, and manage:jira-configuration. Configure these via the default_token_scopes config field, or selectively remove scopes via disabled_scopes — any scope in the disabled list is removed from the effective scope set even if a token would otherwise grant it. Tokens issued through the OAuth 2.0 flow (/authorize and /oauth/token) carry the scopes negotiated during authorization.
Slack twin tier limits
The Slack twin enforces tier-based file upload size limits, letting you test how your application handles free-tier restrictions and paid-tier upgrades. The twin also supports tier-dependent workspace constraints: message history limits, app install limits, and workflow availability.| Tier | Default max file size | Message history | Workflows |
|---|---|---|---|
free | 1 GB | 90 days | Disabled |
pro | 1 GB | Unlimited | Enabled |
business_plus | 1 GB | Unlimited | Enabled |
enterprise | 1 GB | Unlimited | Enabled |
paid | 1 GB | Unlimited | Enabled |
file_too_large error with details about the active tier and cap:
files.getUploadURLExternal with a length that exceeds the cap, and when uploading file content that exceeds the cap during the external upload step.
Switching tiers
Use the admin API to switch between tiers:| Field | Type | Description |
|---|---|---|
tier | string | Current tier: "free", "pro", "business_plus", "enterprise", or "paid". |
max_file_bytes | integer | Maximum file size in bytes for the active tier. |
free_max_file_bytes | integer | Maximum file size in bytes for the free tier. |
paid_max_file_bytes | integer | Maximum file size in bytes for the paid tier (applies to pro, business_plus, enterprise, and paid). |
Slack twin workspace constraints
The Slack twin supports workspace-level settings that let you simulate how your application handles disabled file uploads, storage quota limits, and rate limiting. Configure these via scenario seeding or the admin API.File upload toggle
Setfile_uploads_enabled to false to simulate a workspace where an admin has disabled file uploads. Any call to files.getUploadURLExternal returns a file_uploads_disabled error:
Storage quotas
Setmax_storage_bytes to enforce a workspace-level storage quota. The twin tracks total storage usage (the sum of storage_used_bytes and all uploaded file content) and rejects new uploads when the quota would be exceeded. A call to files.getUploadURLExternal with a length that would push usage over the quota returns a storage_limit_reached error:
| Field | Type | Default | Description |
|---|---|---|---|
max_storage_bytes | integer | null | null (no limit) | Maximum total storage in bytes for the workspace. Set to null to disable the quota. |
storage_used_bytes | integer | 0 | Pre-existing storage usage in bytes. Use this to simulate a nearly-full workspace without uploading actual files. |
max_storage_bytes to 500 and storage_used_bytes to 400 means only 100 bytes of new uploads are allowed before the quota is reached.
Rate limiting
Setrate_limiting_enabled to true and provide a rate_limits map to simulate per-method rate limiting. When a method exceeds its configured request limit within the time window, the twin returns a ratelimited error:
| Field | Type | Default | Description |
|---|---|---|---|
rate_limiting_enabled | boolean | false | Whether rate limiting is active. |
rate_limits | object | {} | Map of method names to rate limit rules. |
rate_limits.<method>.window_seconds | integer | — | Time window in seconds for the rate limit. |
rate_limits.<method>.max_requests | integer | — | Maximum number of requests allowed within the window. |
rate_limits.<method>.retry_after_seconds | integer | — | Value returned in the Retry-After header when rate limited. |
Slack twin configurable token scopes
You can customize the OAuth scopes granted to bot and user tokens in the Slack twin. This lets you test how your application handles missing permissions — for example, verifying that your app gracefully handles amissing_scope error when a required scope is not granted.
Bot tokens always receive
chat:write and files:write as minimum scopes, even if your seed configuration specifies a narrower set. The twin automatically adds these scopes to any bot token that lacks them. This ensures that bots can always send messages and upload files. To test missing_scope errors for chat:write or files:write, use a user token instead.Default scopes
The twin ships with these default scopes:| Token type | Default scopes |
|---|---|
Bot (oauth_bot_scopes) | channels:history, channels:manage, channels:read, chat:write, files:read, files:write, reactions:write, search:read, team:read, users:read |
User (oauth_user_scopes) | search:read, channels:read, channels:history, groups:history, im:history |
Configuring scopes via scenario seeding
Passoauth_bot_scopes, oauth_user_scopes, or tokens in the Slack twin’s seed configuration to override the defaults:
| Field | Type | Description |
|---|---|---|
oauth_bot_scopes | array of string | Scopes granted to bot tokens issued via the OAuth flow. Overrides the default bot scope list. |
oauth_user_scopes | array of string | Scopes granted to user tokens issued via the OAuth flow. Overrides the default user scope list. |
tokens | array of object | Explicit token records to register in the twin. Each token has a token string, user_id, scopes array, and is_bot boolean. Use this to create tokens with specific scope combinations for testing permission edge cases. Bot tokens always receive chat:write and files:write as minimum scopes regardless of the scopes you specify. |
missing_scope error described in authentication handling. Note that bot tokens always retain chat:write and files:write even when you specify a narrower scope list — see the note above.
Scope requirements by API method
The Slack twin enforces the following scope requirements:| API method | Required scope |
|---|---|
chat.postMessage, chat.postEphemeral, chat.update, chat.delete, chat.meMessage | chat:write |
conversations.create, conversations.invite, conversations.kick, conversations.archive, conversations.unarchive, conversations.rename, conversations.setPurpose, conversations.setTopic, conversations.leave, conversations.open, conversations.close, conversations.mark | channels:manage |
conversations.join | channels:join |
conversations.list, conversations.info, conversations.members | channels:read |
conversations.history, conversations.replies | channels:history |
files.upload, files.getUploadURLExternal, files.completeUploadExternal, files.delete, files.sharedPublicURL, files.revokePublicURL | files:write |
files.info, files.list | files:read |
reactions.add, reactions.remove | reactions:write |
reactions.get, reactions.list | reactions:read |
search.messages, search.files, search.all | search:read |
users.info, users.list, users.identity, users.getPresence | users:read |
users.setPresence, users.profile.set | users:write |
users.profile.get | users.profile:read |
users.lookupByEmail | users:read.email |
team.info | team:read |
team.accessLogs | admin |
pins.add, pins.remove | pins:write |
pins.list | pins:read |
bookmarks.add, bookmarks.edit, bookmarks.remove | bookmarks:write |
bookmarks.list | bookmarks:read |
reminders.add, reminders.complete, reminders.delete | reminders:write |
reminders.info, reminders.list | reminders:read |
usergroups.create, usergroups.disable, usergroups.enable, usergroups.update, usergroups.users.update | usergroups:write |
usergroups.list, usergroups.users.list | usergroups:read |
dnd.setSnooze, dnd.endSnooze, dnd.endDnd | dnd:write |
dnd.info, dnd.teamInfo | dnd:read |
emoji.list | emoji:read |
canvases.create, canvases.edit, conversations.canvases.create | canvases:write |
views.open, views.publish, views.push, views.update | commands |
groups.create, groups.invite | groups:write / groups:write.invites |
groups.history, groups.replies | groups:history |
groups.list, groups.info | groups:read |
im.open, im.close, im.mark | im:write |
im.history, im.replies | im:history |
im.list | im:read |
mpim.open, mpim.close, mpim.mark | mpim:write |
mpim.history, mpim.replies | mpim:history |
mpim.list | mpim:read |
api.test and auth.test) do not require a specific scope.
Slack twin admin endpoints
The Slack twin exposes admin endpoints for inspecting and managing twin state during a session.Get config
Replace config (full reset)
SlackTwinConfig object. Any fields you omit revert to their defaults.
Response
Patch config (preserve state)
List users
| Field | Type | Description |
|---|---|---|
users | array | List of user objects in the workspace. |
users[].id | string | User identifier. |
users[].name | string | Username. |
users[].real_name | string | null | Display name. |
users[].is_bot | boolean | Whether the user is a bot. |
users[].deleted | boolean | Whether the user has been deactivated. |
users[].team_id | string | Workspace team identifier. |
users[].tokens | array | Tokens associated with this user. |
users[].tokens[].token | string | The token string. |
users[].tokens[].is_bot | boolean | Whether this is a bot token. |
users[].tokens[].scopes | array of string | OAuth scopes granted to this token. |
File downloads
Uploaded files are accessible via direct download endpoints:Content-Disposition header for download. Returns 404 if the file does not exist.
Google Drive twin storage tiers
The Google Drive twin enforces storage quota limits based on a configurable storage tier, letting you test how your application handles quota-exceeded errors.| Tier | Storage quota |
|---|---|
free | 15 GB |
basic | 100 GB |
premium | 2 TB |
ai_pro | 5 TB |
403 storageQuotaExceeded error matching the real Google Drive API:
about.get endpoint reflects the effective storage quota limit in its storageQuota.limit field.
Switching storage tiers
Use the admin API to get or set the storage tier:storage_quota_limit config field, which takes precedence over the tier-based default.
Dropbox twin account tiers
The Dropbox twin enforces storage quota limits based on a configurable account tier.| Tier | Storage quota |
|---|---|
basic | 2 GB |
plus | 2 TB |
professional | 3 TB |
business | 5 TB |
insufficient_space error matching the real Dropbox API:
get_space_usage endpoint reflects the allocated quota based on the active tier.
Switching account tiers
Use the admin API to get or set the account tier:Notion twin workspace plans
The Notion twin enforces workspace plan-based limits, letting you test how your application handles different plan tiers.| Plan | Max file upload | Max guests |
|---|---|---|
free | 5 MB | 10 |
plus | 5 GB | 100 |
business | 5 GB | 250 |
enterprise | 5 GB | Unlimited |
disabled_capabilities config field for selectively disabling API capabilities.
Switching workspace plans
Use the admin API to get or set the workspace plan:Discord twin boost levels
The Discord twin supports configurable server boost levels that control resource limits.| Boost level | Max file upload | Max emoji slots |
|---|---|---|
0 | 25 MB | 50 |
1 | 25 MB | 100 |
2 | 50 MB | 150 |
3 | 100 MB | 250 |
guild_boost_level config field. The max_file_upload_bytes and max_emoji_slots fields can also be set independently to override the boost-level defaults.
Slack twin conversations.open
The Slack twin supports theconversations.open API method for opening or creating direct-message channels.
| Field | Type | Required | Description |
|---|---|---|---|
users | string | Conditional | Comma-separated list of user IDs to include in the DM. Required when channel is not provided. The authenticated user is automatically added if not already in the list. |
channel | string | Conditional | Channel ID of an existing DM to reopen. When provided, users is ignored. |
| Field | Type | Description |
|---|---|---|
channel | object | The opened or created channel object. |
already_open | boolean | true if the DM already existed. |
no_op | boolean | true if no new channel was created. |

