Metadata-Version: 2.5
Name: robovue-sdk
Version: 0.2.0
Summary: Report RPA and scripted bot runs to Robovue: signed requests, retries, checks before sending and dry runs.
Project-URL: Homepage, https://robovue.co.uk
Project-URL: Documentation, https://robovue.co.uk/api
Author: Robovue
License-Expression: MIT
License-File: LICENSE
Keywords: automation,bots,monitoring,robovue,rpa,webhook
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: System :: Monitoring
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: pytest; extra == 'dev'
Description-Content-Type: text/markdown

# robovue-sdk

Report your bots' runs to [Robovue](https://robovue.co.uk), which watches every bot in one place and
tells you when one fails or doesn't run. This package signs each report, retries when the network
or Robovue has a problem, checks a report against Robovue's rules before sending it, and can send
dry runs while you set it up. It uses only Python's standard library.

## Install

```shell
pip install robovue-sdk
```

## Requirements

- Python 3.9 or later, on any operating system. No other packages.
- A Robovue workspace. Signing up is free.

Can't install a package where your bot runs, such as Power Automate Desktop's built-in Python?
Connections → HTTP API in Robovue gives you the same request as plain Python, C# and shell code to
paste in, and the [wire contract](#the-wire-contract-any-language) below describes it for any
language.

## Quickstart

1. **Sign up** at [robovue.co.uk](https://robovue.co.uk).
2. **Copy your credentials** from **Settings → Connections → HTTP API**: the webhook secret, and
   your tenant ID, which is the long id in the address shown there
   (`https://app.robovue.co.uk/ingest/<tenant ID>/runs`).
3. **Keep them out of your code.** Set them as environment variables on the machine that runs the
   bot, `ROBOVUE_TENANT_ID` and `ROBOVUE_WEBHOOK_SECRET`, or in your platform's credential store,
   and create one client:

<!-- sample:begin imports create-client -->
```python
import os
import uuid
from datetime import datetime, timezone

from robovue_sdk import FailureCategory, RobovueApiException, RobovueClient, RunReport, RunStatus

# One client for the whole process: it's safe to share between threads.
robovue = RobovueClient(
    base_url="https://app.robovue.co.uk",
    tenant_id=os.environ["ROBOVUE_TENANT_ID"],
    webhook_secret=os.environ["ROBOVUE_WEBHOOK_SECRET"],
)
```
<!-- sample:end -->

4. **Wrap your bot's job.** Report the run as `RUNNING` when it starts, then as `SUCCESS` or
   `FAILURE` when it ends, with the same delivery id so Robovue knows it's one run:

<!-- sample:begin quickstart -->
```python
delivery_id = str(uuid.uuid4())  # one per run: it ties the run's reports together
started_at = datetime.now(timezone.utc)

robovue.try_report_run(
    RunReport(
        bot_id="invoice-bot-01",
        platform="python",
        status=RunStatus.RUNNING,
        started_at=started_at,
    ),
    delivery_id=delivery_id,
)

try:
    process_invoices()  # your bot's work
except Exception as exc:
    robovue.try_report_run(
        RunReport(
            bot_id="invoice-bot-01",
            platform="python",
            status=RunStatus.FAILURE,
            started_at=started_at,
            completed_at=datetime.now(timezone.utc),
            failure_category=FailureCategory.APPLICATION_EXCEPTION,
            payload={"error": str(exc)},
        ),
        delivery_id=delivery_id,
    )
    raise

robovue.try_report_run(
    RunReport(
        bot_id="invoice-bot-01",
        platform="python",
        status=RunStatus.SUCCESS,
        started_at=started_at,
        completed_at=datetime.now(timezone.utc),
    ),
    delivery_id=delivery_id,
)
```
<!-- sample:end -->

   `try_report_run` never raises, so a problem reaching Robovue can't break your bot. The job's own
   exception is reported, then raised again, so the bot still fails the way it always did.

5. **Check it arrives.** Run the bot once with [`dry_run`](#checking-a-new-integration-with-a-dry-run)
   on, then for real, and watch the report land in the Verify step on Connections → HTTP API, in the
   request log, and on Fleet.

## Make missed runs alert

A failure alerts as soon as it's reported. A run that never happens can only be noticed if Robovue
knows when to expect it, so in **Manage bots**, register the bot (its id is `bot_id`) and give it a
schedule. Until a bot is registered, its runs appear under **Unregistered ids**, and each new id
counts toward your plan's bot limit.

## Reporting in more detail

### Failure categories

On a `FAILURE`, set `failure_category` to say why it failed. It's shown on the run, filterable on
Runs, and named in the alert. Leave it out if you don't know: Robovue records `Uncategorized`, and a
run can be reclassified later from its page.

<!-- sample:begin failure-category -->
```python
result = robovue.report_run(
    RunReport(
        bot_id="invoice-bot-01",
        platform="python",
        status=RunStatus.FAILURE,
        started_at=started_at,
        completed_at=datetime.now(timezone.utc),
        failure_category=FailureCategory.BUSINESS_EXCEPTION,
    )
)
```
<!-- sample:end -->

| Category | Use it for |
|---|---|
| `BUSINESS_EXCEPTION` | A business rule stopped the run: bad input data, a missing record, a rejected transaction. |
| `APPLICATION_EXCEPTION` | Something broke in the bot or the application it drives. |
| `TIMEOUT` | The run, or a step in it, took too long. |
| `AUTHENTICATION_ERROR` | The bot couldn't sign in to something it needs. |
| `CANCELLED` | A person or a scheduler stopped the run. |
| `UNCATEGORIZED` | You don't know. The default. |

### Attaching context with `payload`

`payload` takes any JSON-serialisable value and is stored as sent, shown in full on the run's page.
Use it for whatever would help you debug the run later: an invoice number, the step that failed, a
stack trace. If it has an `error`, `errorMessage` or `message` key, the alert uses that text to say
what went wrong.

<!-- sample:begin payload -->
```python
result = robovue.report_run(
    RunReport(
        bot_id="invoice-bot-01",
        platform="python",
        status=RunStatus.FAILURE,
        started_at=started_at,
        completed_at=datetime.now(timezone.utc),
        failure_category=FailureCategory.TIMEOUT,
        payload={
            "invoiceId": "INV-2291",
            "error": "Timed out waiting for the vendor portal",
        },
    )
)
```
<!-- sample:end -->

The whole report, payload included, must be under 64 KB.

### Attaching a screenshot or log to a failed run

On the Team plan and above, you can attach one screenshot or log excerpt to a `FAILURE` run once
it's reported. Screenshots are off for each bot until someone turns them on (**Manage bots →
Screenshots**), because a screenshot can capture credentials or other sensitive data on screen.

<!-- sample:begin attachment -->
```python
robovue.upload_attachment(result.run_id, screenshot_bytes, "image/png")
```
<!-- sample:end -->

`content_type` is `"image/png"`, `"image/jpeg"` or `"text/plain"`, up to 5 MB, one per run. Robovue
deletes attachments after 30 days, or 7 or 14 if an Admin chose that. An upload for a bot with
screenshots off, or on a plan without them, gets the same 404 as an unknown run, so catch it if a
missing screenshot shouldn't fail your bot.

### Reporting without ever raising

Monitoring shouldn't become a new way for a bot to fail. `try_report_run` never raises for a failed
report: an invalid report, a rejection or a network problem comes back in the outcome for you to log.

<!-- sample:begin try-report -->
```python
outcome = robovue.try_report_run(report)
if not outcome.succeeded:
    print(f"Couldn't report the run to Robovue: {outcome.error}", file=sys.stderr)
```
<!-- sample:end -->

### When a report is rejected

`report_run` raises `RobovueApiException`. Branch on `code`, which is stable; the message is for
people. `field` names the field at fault when there is one, and `request_id` is worth quoting if you
contact support.

<!-- sample:begin rejected -->
```python
try:
    robovue.report_run(report)
except RobovueApiException as exc:
    if exc.code != "timestamp_out_of_range":
        raise
    # This machine's clock is more than 5 minutes out. The message gives Robovue's time.
    print(exc, file=sys.stderr)
```
<!-- sample:end -->

Network errors and 5xx responses are retried automatically (3 attempts by default), and so is a
`429` after its `Retry-After`, unless that's more than a minute, when the 429 is raised instead of
blocking your bot. A report is checked against the server's own rules before anything is sent, so
most mistakes fail at once with a `ValueError`. A `datetime` with no time zone is treated as UTC.
Every code is in the [responses table](#responses) below, with whether a retry can help.

### Checking a new integration with a dry run

With `dry_run=True`, every request is checked exactly as a real one would be (signature, fields,
plan limits, delivery id) but nothing is stored. The result says what would have happened, and a
request that would fail raises the real error.

<!-- sample:begin dry-run -->
```python
robovue = RobovueClient(
    base_url="https://app.robovue.co.uk",
    tenant_id=tenant_id,
    webhook_secret=webhook_secret,
    dry_run=True,  # checked in full, stored nowhere
)

result = robovue.report_run(report)
print(result.dry_run, result.would_be)  # True created
```
<!-- sample:end -->

Dry runs show in the request log, which warns you if any arrived in the last day, so a bot left in
dry-run mode doesn't go unnoticed. Turn it off before going live.

### Runs that take a while

The quickstart reports `RUNNING` first. That's optional, but it means a bot that starts a job and
then hangs shows as Running straight away, rather than only when its next run fails to turn up.
Reusing the delivery id is the whole mechanism: a fresh id on the second report makes two runs,
one stuck Running and one that looks like nobody started it. A run left Running silences nothing:
if it never completes, the bot still goes Late and then Overdue on its schedule.

## Using it in your bot

**One client per process.** `RobovueClient` holds no state between calls, so share one instance
across threads.

**Timeouts, proxies and TLS.** Each attempt times out after 30 seconds. Requests go through
Python's `urllib`, so they use the proxy in `HTTPS_PROXY` (or, on Windows, the system's proxy
settings) and the operating system's trusted certificates; set `SSL_CERT_FILE` to a certificate
bundle if your network inspects TLS with its own certificate. For anything else, pass
`transport=`: a function that takes the URL, a dict of headers and the body bytes, sends the POST,
and returns `(status, response_body, response_headers)`.

## Options

| `RobovueClient(...)` | Default | What it does |
|---|---|---|
| `base_url` | required | `https://app.robovue.co.uk` |
| `tenant_id` | required | Your workspace's tenant ID. |
| `webhook_secret` | required | Signs every request. Never put it in source code. |
| `max_retry_attempts` | `3` | Attempts per call, the first included. |
| `retry_base_delay` | `0.2` | Seconds before the first retry; each later wait doubles. |
| `dry_run` | `False` | Check every request in full and store nothing. |
| `transport` | `urllib` | How requests are sent (see above). |

## Troubleshooting

| What you see | What to do |
|---|---|
| `invalid_signature` | The secret is wrong or was rotated. Copy the current one from Connections → HTTP API. |
| `timestamp_out_of_range` | The machine's clock is more than 5 minutes out. Sync it (on Windows, `w32tm /resync`). |
| `unknown_tenant` | The tenant ID is wrong. Copy it from the address on Connections → HTTP API. |
| `bot_cap_reached` | A new bot id would go over your plan's bot limit. Delete a bot you no longer use, or change plan. |
| `payload_too_large` | The report is over 64 KB. Send less in `payload`, and attach a log file instead. |
| A 403 with no code | Something between your bot and Robovue blocked the request, such as a proxy rewriting it. Check with whoever runs your network, or send us the time it happened. |
| An `OSError` or `URLError` | The machine can't reach `app.robovue.co.uk`. Check its proxy and firewall. |
| `CERTIFICATE_VERIFY_FAILED` | Your network inspects TLS. Point `SSL_CERT_FILE` at a bundle that includes its certificate. |
| The report succeeds but nothing shows on Fleet | `dry_run` is still on, the bot is archived (see **Manage bots → Archived bots**), or the credentials belong to another workspace. |
| A bot stopped running and nobody was told | It has no schedule. See [Make missed runs alert](#make-missed-runs-alert). |

The request log, in Connections, shows every request from the last 7 days and the exact reason any
was turned away.

## The wire contract (any language)

This is exactly what `RobovueClient` sends. From a language with no Robovue SDK, send the same
request yourself. The tables below are generated from the server's own contract, so they list every
field, header, response and limit, including the optional ones.

<!-- contract:begin — generated from IngestionContract by ContractDocsTests; don't edit by hand -->
### `POST /ingest/{tenantId}/runs`

The endpoint both SDKs call, and the one to use from any other language. A JSON object with the fields below.

#### Headers

| Header | Value | Description |
|---|---|---|
| `Content-Type` | application/json | The body is JSON. A charset parameter is fine. |
| `X-Robovue-Delivery-Id` | 1–200 printable ASCII characters, no spaces | Unique to this run. Sending it again — a retry, or the Success/Failure that ends a Running run — updates or confirms the same run instead of creating a second one. A GUID, or your bot's own run id, both work. |
| `X-Robovue-Timestamp` | Unix time in whole seconds | When you signed the request. It must be within 5 minutes of Robovue's clock either way, so a captured request stops working within minutes. Sign each attempt, retries included, just before sending it. |
| `X-Robovue-Signature` | v1=&lt;lowercase hex&gt; | HMAC-SHA256, keyed with your webhook secret, of the timestamp, the word run, the delivery id — each followed by a line break — then the exact request body; hex-encoded in lowercase and prefixed with v1=. Sign the exact bytes you send: re-serialising the JSON after signing changes them. Several comma-separated v1= values are accepted, and any one matching is enough. |
| `X-Robovue-Dry-Run` | true | Checks the request exactly as a real one — authentication, every field, the delivery id, your plan's bot limit — and stores nothing. The response says what would have happened. Use it to test an integration without creating runs. Any other value, or no header, is an ordinary request. |

#### Body fields

| Field | Type | Required | Description |
|---|---|---|---|
| `botId` | string, ≤ 200 chars | Yes | Your name for the bot. Every run with the same botId is the same bot on your dashboard. Spaces and punctuation are fine; not / or \, no spaces at either end, and no tabs or line breaks. The same id registers and manages the bot through the Management API. |
| `platform` | string, ≤ 50 chars | Yes | What runs the bot, shown beside it on the dashboard — any short label, such as dotnet, python, go or blue-prism. Same whitespace rules as botId. |
| `status` | one of `Success`, `Failure`, `Running` | Yes | How the run went. Running reports a run that has started and not finished yet; send the same delivery id again with Success or Failure when it ends. Values are case-sensitive. |
| `startedAt` | timestamp | Yes | When the run started, as an RFC 3339 timestamp with a time zone offset — Z for UTC or ±hh:mm, e.g. 2026-07-17T09:00:00Z. Not before 2000, and not more than an hour ahead of Robovue's clock. Unix times aren't accepted. |
| `completedAt` | timestamp | Required when status is Success or Failure; leave it out when status is Running | When the run finished. Same format and limits as startedAt, and not before it. |
| `failureCategory` | one of `Uncategorized`, `BusinessException`, `ApplicationException`, `Timeout`, `AuthenticationError`, `Cancelled` | No — only with status Failure | Why the run failed, if your bot can tell. Leave it out if not — it becomes Uncategorized and can be changed later on the run's page. Values are case-sensitive. |
| `payload` | object | No | Anything that will help you debug the run later — an invoice number, the step that failed, a stack trace. Must be a JSON object; it's stored as sent and shown on the run's page. A top-level error, errorMessage or message string becomes the summary in the failure alert. Counts toward the 64 KB request limit. |

#### Responses

Every error body is `{"error": "…", "code": "…", "field": "…", "requestId": "…"}` — `field` only when the error is about one field — and every response carries an `X-Request-Id` header.

| Status | Code | Meaning | Retry? |
|---|---|---|---|
| 201 | — | A new run was recorded. Body: {"runId": "…", "duplicate": false} | No need — nothing more to send |
| 200 | — | The run was already known. duplicate is false when this completed a run reported as Running, and true when it repeated a report already recorded — a retry, which is safe. | No need — nothing more to send |
| 200 | — | A dry run passed every check. Body: {"dryRun": true, "wouldBe": "created"} — or "completed" or "duplicate". Nothing was stored. | No need — nothing more to send |
| 400 | `invalid_body` | The body isn't a single valid JSON object. The message gives the line and position. | No — fix the request first |
| 400 | `duplicate_field` | A field appears twice. | No — fix the request first |
| 400 | `unknown_field` | A field isn't one of the documented names. Names are case-sensitive; put your own data inside payload. | No — fix the request first |
| 400 | `missing_field` | A required field is missing, blank or null. field names it. | No — fix the request first |
| 400 | `invalid_field` | A field's value breaks its rule. field names it and the message says how. | No — fix the request first |
| 400 | `missing_delivery_id` | X-Robovue-Delivery-Id is missing. | No — fix the request first |
| 400 | `invalid_delivery_id` | X-Robovue-Delivery-Id is longer than 200 characters, or has spaces or non-ASCII characters. | No — fix the request first |
| 400 | `bot_cap_reached` | This would be a new bot and your plan's bot limit is reached. Archive and delete a bot you no longer use, or raise the limit on the Billing page. | No — fix the request first |
| 401 | `missing_signature` | X-Robovue-Signature is missing. | No — fix the request first |
| 401 | `missing_timestamp` | X-Robovue-Timestamp is missing. | No — fix the request first |
| 401 | `invalid_timestamp` | X-Robovue-Timestamp isn't whole Unix seconds — milliseconds are the usual cause. | No — fix the request first |
| 401 | `timestamp_out_of_range` | X-Robovue-Timestamp is more than 5 minutes from Robovue's clock. The message gives the server's time; fix the sending machine's clock. | Yes, once re-signed with the current time |
| 401 | `invalid_signature` | The signature doesn't match. Check the secret, and that you sign the timestamp, the delivery or run id and the exact bytes you send. | No — fix the request first |
| 404 | `unknown_tenant` | No workspace has this tenant id. Copy the URL from your Connect page. | No — fix the request first |
| 409 | `delivery_id_conflict` | The delivery id already belongs to a different bot's run. Reuse a delivery id only for the same bot's run. | No — fix the request first |
| 413 | `payload_too_large` | The body is over 64 KB. | No — fix the request first |
| 415 | `unsupported_media_type` | Content-Type isn't application/json. | No — fix the request first |
| 429 | `rate_limited` | Too many requests. Retry-After says how many seconds to wait. | Yes, after the Retry-After seconds |
| 500 | `internal_error` | Something went wrong on Robovue's side. Quote the requestId if it keeps happening. | Yes, with backoff — the same delivery id makes it safe |

#### Limits

| Limit | Value | Notes |
|---|---|---|
| Run report size | 64 KB | Per request, payload included. Anything larger is rejected with 413 — it isn't truncated. |
| Run reports | 600 per minute per workspace | Shared by the run report, Power Automate and UiPath endpoints. A start and its completion count as two. |
| Attachment size | 5 MB | One file per failed run: PNG, JPEG or UTF-8 text. |
| Attachment uploads | 50 per hour per workspace | Across all bots. |
| Attachment storage | 500 MB per workspace | Attachments are deleted after the workspace's retention, 30 days at most, which frees their space. |
| Management API | 300 requests per minute per workspace | Every /tenants/… request. |
| Per address | 3,000 requests per minute per client IP | Across ingestion and the Management API together, for every workspace sending from that address. |
<!-- contract:end -->

**Signing, in one line:** `"v1=" + hex(HMAC-SHA256(key: webhook secret, message: timestamp + "\nrun\n" + delivery id + "\n" + the exact body bytes))`,
lowercase, sent with the same timestamp in `X-Robovue-Timestamp` (Unix seconds). Sign each attempt, retries
included, just before sending it. Sign the bytes you actually send: re-serialising the JSON after signing (a
different key order, different whitespace) changes them and the request is rejected with `invalid_signature`.
Connections → HTTP API has a signature checker to compare yours against.

The attachment upload (`POST /ingest/{tenantId}/runs/{runId}/attachment`) uses the same scheme with the word
`attachment` in place of `run` and the run id (lowercase, hyphenated) in place of the delivery id, computed over
the raw file bytes instead of a JSON body. It has no delivery-id header: one attachment per run is what makes it
safe to retry.

This scheme is only for the `/ingest/{tenantId}/runs` endpoint. UiPath Orchestrator, Power Automate and
Automation Anywhere connect to Robovue without code; Connections in Robovue sets each one up.

## Versions and support

- The package follows [semantic versioning](https://semver.org). `CHANGELOG.md`, in the source
  distribution, lists what changed in each version.
- For bots written in C# or another .NET language, [`Robovue.Sdk`](https://www.nuget.org/packages/Robovue.Sdk)
  is the same client.
- Questions and problems: [contact@robovue.co.uk](mailto:contact@robovue.co.uk). Quote the `RequestId` from
  the error if there is one.
- Found a security problem? See [robovue.co.uk/security](https://robovue.co.uk/security).

## Licence

MIT, so you can use, change and ship this package freely (see `LICENSE` in the package). Using the Robovue
service is covered by the [terms](https://robovue.co.uk/terms).
