Skip to main content
A digital twin is a stateful clone of an external service (Stripe, Slack, GitHub, Dropbox, Google Drive, Google Calendar, Box, Notion, Discord, Waterfall, 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.

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.

Properties

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.
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:
  • UI 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.
For per-twin support details, known limitations, and MCP tool coverage, see the twin reference. The lists below match the twins currently exposed by the app’s provisioning catalog.

UI twins

Backend twins

Twins and integrations are different:
  • Use integrations to connect context sources that help Arga understand your stack.
  • Use digital twins to intercept outbound traffic during sandbox validation.
If you rely on a service not yet supported, book a demo and we’ll discuss building a twin for your stack.

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-Stub header — set to "true" on every stub response.
  • _twin_stub field — a boolean (true) injected into the JSON response body.
  • _twin_warning field — a human-readable string explaining which endpoint is stubbed.
When the response would normally be a JSON array, it is wrapped in an object with _twin_stub, _twin_warning, and a data key containing the original array.
You can use the 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:
Response
To clear the stub hit log:
Returns {"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:
  1. No real side effects — A PR that sends a Slack notification or charges a Stripe card won’t do either during testing.
  2. Deterministic results — Tests produce consistent outcomes regardless of external service availability.
  3. 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, the Authorization 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:
Tokens with the wildcard scope * 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:
You can configure the default token scopes via the 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:
The twin enforces OAuth 2.0 scope requirements on every API call. Each route requires a specific scope — for example, 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:
The default token scopes are 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. When a file upload exceeds the current tier’s limit, the twin returns a file_too_large error with details about the active tier and cap:
This error is returned both when calling 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:
Response
Response

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

Set file_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:
File uploads are enabled by default.

Storage quotas

Set max_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:
For example, setting 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

Set rate_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:

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 a missing_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:

Configuring scopes via scenario seeding

Pass oauth_bot_scopes, oauth_user_scopes, or tokens in the Slack twin’s seed configuration to override the defaults:
When you configure a narrower set of scopes, any API call requiring a scope not in the list returns the 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: Methods not listed (such as 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

Returns the current twin configuration, including token registrations, scope settings, tier, and seed data. Response

Replace config (full reset)

Replace the entire twin configuration and reset all store state (channels, messages, files, etc.). Use this when you need a clean slate. Request body A full SlackTwinConfig object. Any fields you omit revert to their defaults. Response
This endpoint resets the store. Any channels, messages, or files created during the session are lost. If you only need to update specific config fields (such as token scopes) without losing seeded data, use PATCH /admin/config instead.

Patch config (preserve state)

Update individual config fields without resetting the store. Seeded channels, messages, and files remain intact. This is useful when you need to change token scopes or other settings mid-session after data has already been seeded. Request body A partial JSON object containing only the fields you want to update. Fields you omit keep their current values.
Response
Use PATCH when you want to restrict token scopes after seeding sample data. For example, seed a workspace with channels and messages, then patch the config to narrow the bot token’s scopes and verify your app handles missing_scope errors gracefully.

List users

Returns all users in the twin workspace along with their associated tokens. Response

File downloads

Uploaded files are accessible via direct download endpoints:
Both endpoints return the file content with the appropriate MIME type and a 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. When a file upload would exceed the tier’s storage quota, the twin returns a 403 storageQuotaExceeded error matching the real Google Drive API:
The 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:
Response
Response
You can also override the quota limit directly via the 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. When a file upload would exceed the tier’s quota, the twin returns an insufficient_space error matching the real Dropbox API:
The 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:
Response
Response

Notion twin workspace plans

The Notion twin enforces workspace plan-based limits, letting you test how your application handles different plan tiers. When a file upload exceeds the plan’s size limit, the twin returns a validation error. The twin also supports the disabled_capabilities config field for selectively disabling API capabilities.

Switching workspace plans

Use the admin API to get or set the workspace plan:
Response
Response

Discord twin boost levels

The Discord twin supports configurable server boost levels that control resource limits. Configure the boost level via the 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 the conversations.open API method for opening or creating direct-message channels.
Request body Response (new DM)
Response (existing DM)