Skip to main content

Using the Qvasa MCP Tools Over Plain HTTP (No MCP Client Required)

Using the Qvasa MCP Tools Over Plain HTTP (No MCP Client Required)

This is a technical reference for AI agents and developers who want to use Qvasa's MCP capabilities without installing the Qvasa MCP connector. Every tool the hosted MCP server exposes is a thin wrapper over one HTTP endpoint in Qvasa's agentic API, so anything you can do through the MCP you can do with a User Access Token and an HTTP client. This article gives you the exact endpoints, headers, request bodies, and response shapes for the portal tools, plus how to reach everything else through the API's built-in discovery.

If you are an AI agent reading this: the flow is token → session → authenticated calls. Every response you get back carries an explanation and usually a next_step with the exact method and path to call next. Read them; they are written for you.

At a glance

Base URL

https://www.qvasa.com

API namespace

/user_access/api/* (JSON in, JSON out)

Credential

A User Access Token, prefix qva_uat_, sent as Authorization: Bearer <token>

Session

A 24-hour session token, prefix qva_uas_, sent as X-User-Access-Session: <token> on every call except the one that creates it

Alternative transport

JSON-RPC 2.0 to POST /mcp with the same Bearer token (see the last section)

Rate limits

120 calls per minute per session, 20 per minute for expensive calls (metric kickoffs, AI jobs); a 429 carries Retry-After

Before you start

  • You need a User Access Token. A human generates one in Qvasa under User Settings → User Access Tokens; the raw token is shown exactly once. See User Access Tokens and the Qvasa MCP for enabling the feature, granting the permission, and choosing scopes.

  • A token can never do more than its owning user can do in Qvasa. Scopes are intersected with the user's live permissions on every request, so a 403 is a permissions boundary to report to the user, not a bug to retry.

  • Send Content-Type: application/json on requests with a body. All examples below use curl; any HTTP client works.

  • For the security model behind all of this (token storage, scopes, tenancy, auditing), see Qvasa MCP Server — Security and Integration Reference.

Step 1: Create a session with your token

Qvasa uses two-token authentication. The long-lived User Access Token proves who you are; a short-lived session token authorizes every tool call. POST /user_access/api/sessions is the only endpoint that accepts the User Access Token alone, and it is the required first call.

curl -X POST https://www.qvasa.com/user_access/api/sessions \
  -H "Authorization: Bearer qva_uat_YOUR_TOKEN" \
  -H "Content-Type: application/json"

A 200 response has this shape (the toolkit object is large and is abbreviated here):

{
  "session_id": 1234,
  "session_token": "qva_uas_...",
  "session_token_expires_at": "2026-09-18T18:48:33Z",
  "toolkit_version": "2026-09-16.user_access_api_qvasa_trigger_summary_multi_line_target_v1",
  "toolkit": {
    "description": "...",
    "headers_required": {
      "Authorization": "Bearer <your qva_uat_... token>",
      "X-User-Access-Session": "<the session_token returned with this manifest>"
    },
    "request_conventions": { "strict_bodies": "...", "batch": "..." },
    "tools": [ { "name": "sessions", "method": "POST", "path": "/user_access/api/sessions", "description": "...", "paths": [...], "required_scopes": [...] }, ... ],
    "session_lifecycle": { ... }
  },
  "capability_profile": {
    "shape": "full_authoring",
    "summary": "...",
    "what_you_can_do": [...],
    "what_you_cannot_do": [],
    "next_step": { "method": "GET", "path": "/user_access/api/dynamic_dashboards", "description": "..." }
  }
}

What to keep from this response:

  • session_token: send it as X-User-Access-Session on every subsequent call. It expires after 24 hours.

  • toolkit.tools: the manifest of tool groups this token can actually call, already filtered to the token's effective scopes and the user's permissions. A group that is not listed is unavailable to you. Do not memorize this list across sessions; fetch it fresh each time.

  • capability_profile: your boundaries in plain language. shape is full_authoring for users who can build metrics from scratch, or dashboard_scoped for users limited to dashboards they were granted.

Session rules worth knowing:

  • One active session per token. Creating a new session invalidates the previous one. To refresh an existing session instead, send a body of {"attach_to_session_id": <session_id>}.

  • Sessions pin the toolkit version. When Qvasa ships an agent-visible change, existing sessions return 401 with error_code: "session_stale". Recover by POSTing /sessions again and re-reading the manifest.

  • Revoke a session with DELETE /user_access/api/sessions/:session_id (token header only; returns 204).

If you call /sessions with no Authorization header you get 401 with error_code: "missing_user_access_token". If the token is invalid, expired, or revoked you get 401 with the message You must provide a Bearer token to access this resource. UAT401.

Step 2: Send both headers on every other call

Every endpoint other than /sessions requires both headers. Missing the session header returns 401 with error_code: "missing_session_token". A good first authenticated call is GET /user_access/api/me, which tells you which account and user the token belongs to and, critically, which scopes are actually effective:

curl https://www.qvasa.com/user_access/api/me \
  -H "Authorization: Bearer qva_uat_YOUR_TOKEN" \
  -H "X-User-Access-Session: qva_uas_YOUR_SESSION_TOKEN"

{
  "account": { "id": 2, "name": "acme" },
  "user":    { "id": 179, "email": "[email protected]", "name": "Agent Owner" },
  "token": {
    "id": 23,
    "name": "Claude MCP",
    "expires_at": "2026-12-16T18:46:53.401Z",
    "granted_scopes":   [ "can_read_all_endpoints", "can_write_all_endpoints", "can_inspect_objects", ... ],
    "effective_scopes": [ "can_read_all_endpoints", "can_write_all_endpoints", "can_read_open_dashboards", ... ],
    "scopes_filtered_by_user_permissions": [ "can_inspect_objects", ... ]
  },
  "explanation": "Identity + scopes for this token. `token.effective_scopes` is the intersection of the token's granted bits and the user's live permissions ...",
  "next_step": { "method": "POST", "path": "/user_access/api/tools/search", "description": "..." }
}

Check effective_scopes before attempting a gated action rather than discovering the gate by hitting a 403. scopes_filtered_by_user_permissions lists grants the token carries but the user cannot exercise.

Step 3: The MCP portal tools and their HTTP equivalents

The hosted MCP server exposes nine tools. They are a portal, not the full surface: the pattern is search → read the tool guide → call the endpoint. Each row below is exactly what the MCP server does when the tool is invoked, so calling the HTTP endpoint directly gives identical behavior.

MCP tool

HTTP equivalent

Body / notes

qvasa_create_session

POST /user_access/api/sessions

Step 1 above. Token header only.

qvasa_overview

GET /user_access/api/guidance/overview

No body. Orientation packet: positioning, agent contract, workflow index.

qvasa_get_workflow

GET /user_access/api/guidance/workflows/:intent_id

Ordered recipe for one intent.

qvasa_get_tool_guide

GET /user_access/api/guidance/tools/:tool_name

Full prose, paths, and required scopes for one tool group.

qvasa_search_tools

POST /user_access/api/tools/search

{"query": "<natural language>"}

qvasa_query_metric

POST /user_access/api/quick_stats (or basic_charts, donut_charts, multi_data_charts, heat_maps), then GET /user_access/api/metric_requests/:uuid

{"metric_name": "<widget_name>", "filters_json": {...}}. Kickoff returns 202; poll until terminal.

qvasa_call_api

Any GET/POST/PUT/PATCH/DELETE on /user_access/api/*

The same request with the two headers. Nothing outside the namespace is reachable.

qvasa_submit_support_ticket

POST /user_access/api/qvasa_support_tickets

{"ticket_type": "...", "body": "...", "subject"?: "...", "context"?: {...}}

qvasa_list_support_tickets

POST /user_access/api/qvasa_support_tickets/index

{"filters": {"unread_replies_only": true, "open_only": true}}

All of these require only the baseline can_read_all_endpoints scope, except that qvasa_query_metric additionally requires the user to have dashboard authoring access, and qvasa_call_api inherits whatever scope the target endpoint declares.

qvasa_overview → GET /guidance/overview

Call this first in a fresh conversation. It returns a compact packet (under 8 KB) with positioning, an agent_contract of do's and don'ts, a when_to_use list, a workflows index of intent ids with one-line triggers, the mcp_tools list, and next_steps. Users with dashboard-scoped access receive a shorter packet with a capability_profile block instead.

curl https://www.qvasa.com/user_access/api/guidance/overview \
  -H "Authorization: Bearer $UAT" -H "X-User-Access-Session: $SESSION"

qvasa_get_workflow → GET /guidance/workflows/:intent_id

Fetches one recipe as an ordered list of steps. Intent ids currently include metric_count_or_rate, metric_by_dimension_ranked, trend_over_time, executive_summary, answer_needs_classifier, clean_up_reason_codes, ticket_drill_in, reverse_lookup_value, browse_catalog, fill_component_configurations, build_formula_widget, setup_ticket_field_sla, manage_in_flight, resolve_date_range, troubleshoot_error_or_no_data, capability_not_found, and submit_feedback. The overview's workflows array is the authoritative list. An unknown id returns 404 with the full menu in available.

GET /user_access/api/guidance/workflows/metric_count_or_rate{
  "trigger": "user asks 'how many X' / 'what's the rate' / 'show me a count or breakdown'",
  "steps": [
    "qvasa_search_tools — search for 'saved widgets' or 'dashboard widgets' FIRST; ...",
    "qvasa_search_tools — query 'widgets' / 'metrics' / the user's phrasing ...",
    "qvasa_get_tool_guide(tool_name='widgets') — read the widget tool's playbook ...",
    "qvasa_call_api — POST /user_access/api/tools/widgets/search with {query} to resolve a widget; GET /user_access/api/tools/widgets/<widget_name> ...; POST /user_access/api/filters/date_and_time/discover with {phrase} for the date filter.",
    "qvasa_query_metric — kicks off the async metric ... and polls until terminal."
  ],
  "intent_id": "metric_count_or_rate"
}

When a step says qvasa_call_api, make the named HTTP request directly. When it says qvasa_query_metric, follow the kickoff-and-poll procedure below.

qvasa_get_tool_guide → GET /guidance/tools/:tool_name

Returns the manifest entry for one tool group: name, method, path, a long description that doubles as the playbook, every paths entry in the group, and required_scopes. Tool group names are the name values in toolkit.tools from Step 1, for example widgets, filters, metric_requests, ticket_sets, dashboards, dashboard_widgets, qvasa_support_tickets, batch, runbooks. An unknown name returns 404 with the full list in available.

Note the distinction: /guidance/tools/:tool_name describes a tool group. Individual tool definitions with a body_shape and playbook (for example count_tickets or create_dynamic_dashboard) live at GET /user_access/api/tools/:name, which is where search results point.

The primary discovery verb. Pass the user's phrasing; the catalog is already filtered to what this token can call.

curl -X POST https://www.qvasa.com/user_access/api/tools/search \
  -H "Authorization: Bearer $UAT" -H "X-User-Access-Session: $SESSION" \
  -H "Content-Type: application/json" \
  -d '{"query": "how many tickets were created"}'

{
  "query": "how many tickets were created",
  "tool_results": [
    {
      "name": "count_tickets",
      "summary": "Get a single number — how many tickets match a given filter scope.",
      "score": 25,
      "match_reasons": [ "tag token overlap: \"how many tickets\" (2)", ... ],
      "final_endpoint": { "method": "POST", "path": "/user_access/api/quick_stats" },
      "next_step": { "method": "GET", "path": "/user_access/api/tools/count_tickets", "description": "Fetch the full playbook for count_tickets." }
    },
    ...
  ],
  "metric_routing": { ... },
  "construction_paths": { ... },
  "fallback_results": { ... },
  "saved_widget_results": [ ... ],
  "runbook_results": [ ... ],
  "explanation": "Top tools by match score. Pick one and call its next_step ... IMPORTANT: this reads as an analytics/metric question — metrics are NOT tools. See metric_routing ..."
}

How to read the response:

  • tool_results are ranked tool definitions. Trust a hit with score of 75 or higher or a fitting final_endpoint; otherwise GET its next_step.path to read the body_shape and playbook.

  • metric_routing appears when the query is really a metric question. Metrics are widgets, not tools: resolve one via POST /user_access/api/tools/widgets/search with {"query": "..."}, then run it with the metric procedure below.

  • saved_widget_results are the user's pre-built saved queries. Prefer re-running one (POST /user_access/api/dashboard_widgets/:id/query) over building from scratch.

  • runbook_results are pre-built multi-step recipes. Prefer a matching runbook over hand-assembling a sequence; fetch one with GET /user_access/api/runbooks/:name.

  • fallback_results lists widgets and filters that fuzzy-matched when no tool did.

For a last-resort browse, GET /user_access/api/tools returns the exhaustive catalog of tools you can call.

qvasa_query_metric → kickoff, then poll

Metric calculation is asynchronous. The MCP tool hides this behind a single call; over HTTP you do it in two steps.

1. Kick off. Choose the endpoint by chart type and POST the widget name (this is the metric_name) plus optional filters:

metric_kind

Kickoff endpoint

quick_stat

POST /user_access/api/quick_stats

basic_chart

POST /user_access/api/basic_charts

donut_chart

POST /user_access/api/donut_charts

multi_data_chart

POST /user_access/api/multi_data_charts

heat_map

POST /user_access/api/heat_maps

curl -X POST https://www.qvasa.com/user_access/api/quick_stats \
  -H "Authorization: Bearer $UAT" -H "X-User-Access-Session: $SESSION" \
  -H "Content-Type: application/json" \
  -d '{"metric_name": "tickets_created"}'

HTTP 202 Accepted
{
  "async_request_uuid": "aabc73b0-4751-49bf-94cd-fec757a39a10",
  "metric_kind": "quick_stat",
  "request_kind": "metric",
  "status": "enqueued",
  "enqueued_at": "2026-09-17T18:48:38.982Z",
  "next_step": {
    "method": "GET",
    "path": "/user_access/api/metric_requests/aabc73b0-4751-49bf-94cd-fec757a39a10",
    "first_poll_after_ms": 5000,
    "retry_after_ms": 5000,
    "description": "Poll until status is 'completed', 'errored', or 'cancelled'. ..."
  },
  "cancel_step":  { "method": "POST", "path": "/user_access/api/metric_requests/aabc73b0-.../cancel", ... },
  "summary_step": { "method": "GET",  "path": "/user_access/api/metric_requests", ... },
  "applied_defaults": { ... }
}

2. Poll GET /user_access/api/metric_requests/:uuid until status is completed, errored, or cancelled. The MCP server polls every 1.5 seconds for up to 12 seconds; the kickoff's next_step suggests 5-second intervals. Most quick stats finish in a few seconds.

{
  "async_request_uuid": "aabc73b0-4751-49bf-94cd-fec757a39a10",
  "metric_kind": "quick_stat",
  "status": "completed",
  "completed_at": "2026-09-17T18:48:42.468Z",
  "result": {
    "metric_name": "tickets_created",
    "metric_type": "quick_stat",
    "widget_name": "Tickets Created Last 7 Days",
    "current_value": 268,
    "display_value": 268,
    "start_datetime": "2026-09-10T07:00:00.000+00:00",
    "end_datetime":   "2026-09-18T06:59:59.999+00:00",
    "historical_value": 0,
    "percentage_change": "1000 (max)",
    "directional_change": "up",
    "up_is_good": false,
    "filters_applied": { "filters_date_and_time": "{\"time_unit\":\"days\",\"time_length\":\"7\"}" },
    "applied_filters_summary": [ { "type": "include", "label": "Date & Time", "values": [ "Last 7 days" ] } ]
  }
}

Rules that save round trips:

  • metric_name must be a widget name from this account's library. Resolve it with POST /user_access/api/tools/widgets/search ({"query": "tickets created"}) and read the widget's detail at GET /user_access/api/tools/widgets/:widget_name for its widget_type, required filters, and component_configurations. An unknown name returns 404 unknown_metric with that search endpoint as next_step.

  • Omit filters_json and the metric defaults to the last 7 days (reported in applied_defaults).

  • Never hand-build a date filter. POST /user_access/api/filters/date_and_time/discover with {"phrase": "last 30 days"} returns a ready filters_fragment; merge it verbatim into filters_json. Other filters come from POST /user_access/api/tools/filters/search and their own filters_fragment responses. Filter fragments are opaque: never edit their keys or values.

  • Drill into the tickets behind a number with the inspect variants: POST /quick_stats_inspect, /basic_charts_inspect, /donut_charts_inspect take the same body and materialize a ticket set. The completed poll result carries ticket_set_id and ticket_count; read it via GET /user_access/api/ticket_sets/:id, /ticket_sets/:id/tickets_table, and /ticket_sets/:id/summaries.

  • GET /user_access/api/metric_requests lists this session's in-flight requests; POST /metric_requests/:uuid/cancel cancels one that is still enqueued.

qvasa_call_api → any request under /user_access/api/

The MCP's generic dispatcher simply forwards method, path, and body to the API with the two auth headers attached. Over HTTP, that is just the request itself. Allowed verbs are GET, POST, PUT, PATCH, DELETE; every path starts with /user_access/api/. Each endpoint enforces its own scopes, so the dispatcher grants no extra authority.

Conventions that apply to every endpoint you reach this way:

  • Write bodies are strict. Any key the endpoint does not declare is rejected with 422 unknown_body_keys, listing unknown_keys and allowed_keys, and nothing is written. Read the tool's body_shape at GET /user_access/api/tools/:name first.

  • List endpoints are usually POST .../index with an optional filters object and cursor pagination (pagination.next_cursor, has_more).

  • Batch writes: POST /user_access/api/batch runs up to 50 batch-safe operations in one call with {"mode": "all_or_nothing" | "best_effort", "operations": [{"method": "POST", "path": "/user_access/api/...", "body": {...}}, ...]}. Requires can_write_all_endpoints.

  • Every success envelope carries the resource, an explanation, and usually a next_step. Every error envelope carries error, error_code, and often next_step and diagnosis.

curl -X POST https://www.qvasa.com/user_access/api/dynamic_dashboards \
  -H "Authorization: Bearer $UAT" -H "X-User-Access-Session: $SESSION" \
  -H "Content-Type: application/json" \
  -d '{"name": "x", "bogus": 1}'HTTP 422
{
  "error": "Unknown body key(s) [\"bogus\"]. UAF422K",
  "error_code": "unknown_body_keys",
  "unknown_keys": [ "bogus" ],
  "allowed_keys": [ "name", "description", "dashboard_access_type" ],
  "explanation": "Write bodies are strict: every key must be one this endpoint declares. Nothing was written. ...",
  "next_step": { "method": "GET", "path": "/user_access/api/tools/create_dynamic_dashboard", "description": "Read this endpoint's body_shape, then resend with only declared keys." },
  "diagnosis": { "summary": "...", "explain_to_user": "..." }
}

qvasa_submit_support_ticket and qvasa_list_support_tickets

The escape hatch is a two-way support loop with the Qvasa team. File a ticket for a missing tool, an unrecoverable error (echo any support_code from the error into context.support_code), or a capability request. Only can_read_all_endpoints is required.

POST /user_access/api/qvasa_support_tickets
{
  "ticket_type": "bug_report",
  "subject": "quick_stats returns 500 for widget X",
  "body": "Steps to reproduce ...",
  "context": { "support_code": "UAA500X" }
}

ticket_type is one of bug_report, unexpected_behavior, friction_point, feature_request, impossibility_report, question, other. The response also runs a tool search against your text, so if the capability actually exists you find out in the same turn. Then:

  • POST /user_access/api/qvasa_support_tickets/index with {"filters": {"unread_replies_only": true}} answers "did Qvasa get back to me?" in one call. Other filters: status, ticket_type, open_only, mine_only, search.

  • GET /user_access/api/qvasa_support_tickets/:id returns the full thread and marks the reply read.

  • POST /user_access/api/qvasa_support_tickets/:id/messages with {"body": "..."} replies, and PATCH /user_access/api/qvasa_support_tickets/:id/status with {"status": "open" | "closed"} reopens or closes.

Error envelopes

Every error is JSON with a human error string that ends in a support code (for example UAS401M) and a machine error_code. Read error_code and next_step, then act; the recovery hints are designed for in-conversation use.

HTTP

error_code

Meaning and recovery

401

missing_user_access_token

No Authorization header on /sessions. Add Bearer qva_uat_....

401

(message UAT401)

Token invalid, expired, or revoked. The user must generate a new one.

401

missing_session_token

No X-User-Access-Session header. Create a session (Step 1).

401

invalid_session_token

Session expired, revoked, or superseded by a newer session. POST /sessions again.

401

session_stale

Toolkit was updated since this session started. POST /sessions again and re-read the manifest.

403

permission_denied

denial_layer is foundational_gate (feature disabled for the account or user), token_scope (see missing_scopes), user_permission, group_access, or resource_permission (a specific dashboard or object). Not retryable; tell the user which permission is missing.

404

unknown_metric

metric_name is not a widget on this account. Use next_step (widget search).

404

no_route / agentic_tool_not_found

Wrong path or a tool this token cannot see. GET /user_access/api/tools lists what you can call.

422

unknown_body_keys, missing_required_param, invalid_enum_value, invalid_filters_date_and_time

Body did not match the contract. Compare with allowed_keys or the tool's body_shape; resend.

429

rate_limit_exceeded

More than 120 calls per minute, or more than 20 expensive calls per minute, in this session. Wait for Retry-After seconds.

500

unexpected_error

Carries a support_code. File a support ticket and include it in context.support_code.

GET /user_access/api/guidance/errors/:error_code explains the domain-specific codes that can appear inside a completed metric's result (for example a dashboard widget's error banner); the transport-level codes above are documented in this table instead.

Alternative: call the MCP server directly with JSON-RPC

If you would rather speak MCP without an MCP client library, the hosted server at POST https://www.qvasa.com/mcp is a stateless Streamable HTTP endpoint that returns plain JSON. It accepts the same qva_uat_ token as a Bearer credential (OAuth is optional and only needed by clients that want the connect-by-URL flow). There is no session header to manage: the server creates or reuses the token's session internally on every request.

curl -X POST https://www.qvasa.com/mcp \
  -H "Authorization: Bearer qva_uat_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}'

The result lists the nine tools with their inputSchema. Invoke one with tools/call:

curl -X POST https://www.qvasa.com/mcp \
  -H "Authorization: Bearer qva_uat_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0", "id": 2, "method": "tools/call",
    "params": {
      "name": "qvasa_query_metric",
      "arguments": { "metric_kind": "quick_stat", "metric_name": "tickets_created" }
    }
  }'{
  "jsonrpc": "2.0", "id": 2,
  "result": {
    "content": [ { "type": "text", "text": "{\"async_request_uuid\":\"...\",\"status\":\"completed\",\"result\":{\"metric_name\":\"tickets_created\",\"current_value\":268, ...}}" } ]
  }
}

  • Tool results arrive as a JSON string inside result.content[0].text; parse it. If the underlying HTTP call returned 4xx or 5xx, the response is flagged as an error and the text is the same error envelope described above.

  • An initialize handshake is accepted but not required; each request is independent.

  • Server-Sent Events are not supported; keep the Accept header as shown and expect a plain JSON body.

  • An unauthenticated request returns 401 with a WWW-Authenticate header pointing at /.well-known/oauth-protected-resource; you can ignore that and send the Bearer token.

  • qvasa_query_metric over this transport polls for you for up to 12 seconds; if the metric is still calculating it returns status: "processing" with the metric_requests/:uuid path to poll via qvasa_call_api.

A complete minimal sequence

# 1. Session
SESSION=$(curl -s -X POST https://www.qvasa.com/user_access/api/sessions \
  -H "Authorization: Bearer $UAT" | jq -r .session_token)
AUTH=(-H "Authorization: Bearer $UAT" -H "X-User-Access-Session: $SESSION" -H "Content-Type: application/json")# 2. Orientation and identity
curl -s https://www.qvasa.com/user_access/api/guidance/overview "${AUTH[@]}"
curl -s https://www.qvasa.com/user_access/api/me "${AUTH[@]}"# 3. Find the metric for the question
curl -s -X POST https://www.qvasa.com/user_access/api/tools/widgets/search "${AUTH[@]}" \
  -d '{"query": "tickets created"}'# 4. Resolve the date range the user asked for
curl -s -X POST https://www.qvasa.com/user_access/api/filters/date_and_time/discover "${AUTH[@]}" \
  -d '{"phrase": "last 30 days"}'        # returns filters_fragment; merge it into filters_json# 5. Run it and poll
UUID=$(curl -s -X POST https://www.qvasa.com/user_access/api/quick_stats "${AUTH[@]}" \
  -d '{"metric_name": "tickets_created", "filters_json": { ...filters_fragment... }}' | jq -r .async_request_uuid)
sleep 5
curl -s https://www.qvasa.com/user_access/api/metric_requests/$UUID "${AUTH[@]}"

Did this answer your question?