{"id":51402944,"url":"https://github.com/testingbot/testingbot-dotnet","last_synced_at":"2026-07-04T08:32:53.074Z","repository":{"id":362643270,"uuid":"1259531784","full_name":"testingbot/testingbot-dotnet","owner":"testingbot","description":".NET/C# api client for TestingBot","archived":false,"fork":false,"pushed_at":"2026-06-05T07:59:57.000Z","size":102,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-05T08:15:26.106Z","etag":null,"topics":["playwright","testingbot","testingbot-api"],"latest_commit_sha":null,"homepage":"https://testingbot.com/support/api","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/testingbot.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-04T15:47:11.000Z","updated_at":"2026-06-05T07:59:47.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/testingbot/testingbot-dotnet","commit_stats":null,"previous_names":["testingbot/testingbot-dotnet"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/testingbot/testingbot-dotnet","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/testingbot%2Ftestingbot-dotnet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/testingbot%2Ftestingbot-dotnet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/testingbot%2Ftestingbot-dotnet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/testingbot%2Ftestingbot-dotnet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/testingbot","download_url":"https://codeload.github.com/testingbot/testingbot-dotnet/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/testingbot%2Ftestingbot-dotnet/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35115741,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-04T02:00:05.987Z","response_time":113,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["playwright","testingbot","testingbot-api"],"created_at":"2026-07-04T08:32:52.230Z","updated_at":"2026-07-04T08:32:53.064Z","avatar_url":"https://github.com/testingbot.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TestingBot .NET SDK\n\n[![NuGet](https://img.shields.io/nuget/v/TestingBot.Api.svg)](https://www.nuget.org/packages/TestingBot.Api/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n\nThe official .NET SDK for the [TestingBot](https://testingbot.com) REST API. Manage tests, builds,\napp storage, tunnels, devices, screenshots, Codeless tests/suites, jobs, and team accounts from C#.\n\n- **Async** all the way down, with `CancellationToken` support everywhere\n- **Thread-safe** — construct one client and reuse it across your application\n- **Strongly typed** models — no `dynamic`, no untyped dictionaries\n- **Zero third-party dependencies** in the core package\n- **Resilient** — automatic retries with exponential backoff and `Retry-After` support\n- Optional **dependency-injection** package wiring up `IHttpClientFactory`\n- Targets **.NET 8** and **.NET 9**\n\n## Packages\n\n| Package | Purpose |\n|---|---|\n| [`TestingBot.Api`](https://www.nuget.org/packages/TestingBot.Api/) | Core client and models (zero third-party dependencies) |\n| [`TestingBot.Api.DependencyInjection`](https://www.nuget.org/packages/TestingBot.Api.DependencyInjection/) | `AddTestingBot()` for `IServiceCollection` / `IHttpClientFactory` |\n\n## Installation\n\n```bash\ndotnet add package TestingBot.Api\n# optional, for ASP.NET Core / Worker apps:\ndotnet add package TestingBot.Api.DependencyInjection\n```\n\n## Authentication\n\nProvide your [API key and secret](https://testingbot.com/members/user/edit) directly, or let the SDK\nresolve them (in order) from a `~/.testingbot` file (`key:secret`), the `TESTINGBOT_KEY`/\n`TESTINGBOT_SECRET` environment variables, or `TB_KEY`/`TB_SECRET`.\n\n```csharp\nusing TestingBot.Api;\n\nvar client = new TestingBotClient(\"YOUR_KEY\", \"YOUR_SECRET\");\n// or resolve from ~/.testingbot or environment variables:\nvar client = TestingBotClient.FromEnvironment();\n```\n\n## Quick start\n\n```csharp\nusing TestingBot.Api;\n\nusing var client = new TestingBotClient(\"YOUR_KEY\", \"YOUR_SECRET\");\n\n// Who am I?\nvar user = await client.User.GetAsync();\nConsole.WriteLine($\"{user.Email} — {user.Seconds} seconds remaining\");\n\n// A single test, by id or WebDriver session id\nvar test = await client.Tests.GetAsync(\"a1b2c3d4-...\");\nConsole.WriteLine($\"{test.Name}: {test.Success}\");\n\n// Mark a test passed and tag it\nawait client.Tests.UpdateAsync(test.SessionId!, new TestUpdate\n{\n    Success = true,\n    StatusMessage = \"All assertions passed\",\n    Build = \"ci-1421\",\n    Groups = [\"smoke\", \"checkout\"],\n});\n```\n\n## Pagination\n\nList endpoints return a `TestingBotPage\u003cT\u003e` (the items plus `meta`), or you can stream every item\nacross all pages with `ListAllAsync`:\n\n```csharp\n// One page at a time\nvar page = await client.Tests.ListAsync(new TestListOptions { Count = 50, Build = \"ci-1421\" });\nConsole.WriteLine($\"{page.Count} of {page.Meta.Total}\");\n\n// Or iterate everything (pages fetched lazily)\nawait foreach (var t in client.Tests.ListAllAsync(new TestListOptions { Build = \"ci-1421\" }))\n{\n    Console.WriteLine($\"{t.Id}: {t.Success}\");\n}\n```\n\n## Uploading apps (storage)\n\nUploads are streamed (never fully buffered in memory), support progress reporting, and use a longer\ntimeout than ordinary calls.\n\n```csharp\nvar progress = new Progress\u003clong\u003e(bytes =\u003e Console.WriteLine($\"{bytes:N0} bytes sent\"));\nvar app = await client.Storage.UploadAsync(new FileInfo(\"app.apk\"), progress);\nConsole.WriteLine(app.AppUrl); // tb://\u003cappkey\u003e — pass as the `app` capability\n\n// Replace the binary behind an app key without changing the tb:// URL:\nawait client.Storage.ReplaceAsync(app.AppKey!, new FileInfo(\"app-new.apk\"));\n```\n\n## Codeless tests and jobs\n\n```csharp\nvar run = await client.CodelessSuites.TriggerAsync(suiteId);\nJob job = await client.Jobs.WaitForCompletionAsync(run.JobId, timeout: TimeSpan.FromMinutes(10));\nConsole.WriteLine(job.Success);\n```\n\n## Error handling\n\nEvery failure derives from `TestingBotException`; catch a specific type to react to a failure mode:\n\n```csharp\ntry\n{\n    await client.Tunnels.StopAsync(tunnelId);\n}\ncatch (TestingBotRateLimitException ex)\n{\n    await Task.Delay(ex.RetryAfter ?? TimeSpan.FromSeconds(30));\n}\ncatch (TestingBotAuthenticationException)\n{\n    Console.Error.WriteLine(\"Check your API key and secret.\");\n}\ncatch (TestingBotValidationException ex)\n{\n    foreach (var error in ex.ValidationErrors)\n    {\n        Console.Error.WriteLine(error);\n    }\n}\n```\n\n| Exception | HTTP status |\n|---|---|\n| `TestingBotAuthenticationException` | 401 |\n| `TestingBotPaymentRequiredException` | 402 (insufficient credits) |\n| `TestingBotForbiddenException` | 403 (read-only account / not admin) |\n| `TestingBotNotFoundException` | 404 |\n| `TestingBotValidationException` | 400 |\n| `TestingBotRateLimitException` | 429 (carries `RetryAfter`) |\n| `TestingBotApiException` | other 4xx/5xx, transport failures |\n| `TestingBotConfigurationException` | missing credentials / invalid options (no HTTP) |\n\n## Dependency injection\n\n```csharp\nusing Microsoft.Extensions.DependencyInjection;\n\nbuilder.Services.AddTestingBot(options =\u003e\n{\n    options.ApiKey = builder.Configuration[\"TestingBot:ApiKey\"];\n    options.ApiSecret = builder.Configuration[\"TestingBot:ApiSecret\"];\n});\n\n// or bind from configuration:\nbuilder.Services.AddTestingBot(builder.Configuration.GetSection(\"TestingBot\"));\n```\n\nThen inject `ITestingBotClient` anywhere. The client is registered as a singleton backed by\n`IHttpClientFactory` with the SDK's authentication and retry handlers.\n\n## Configuration\n\n| Option | Default | Description |\n|---|---|---|\n| `BaseAddress` | `https://api.testingbot.com/v1/` | Override for sandbox/private deployments |\n| `Timeout` | 100s | Per-request timeout for ordinary calls |\n| `UploadTimeout` | 30m | Timeout for storage uploads |\n| `MaxRetries` | 3 | Retries for transient failures (429/5xx) on idempotent requests |\n| `RetryBaseDelay` | 1s | Base delay for exponential backoff |\n| `RespectRetryAfter` | `true` | Honor the server's `Retry-After` header |\n| `DefaultPageSize` | 50 | Page size used by `ListAllAsync` helpers |\n\n## API coverage\n\n`Tests`, `Builds`, `Storage`, `Screenshots`, `Tunnels`, `Devices`, `Browsers`, `CodelessTests`,\n`CodelessSuites`, `Jobs`, `Team`, `User`, `Configuration`, plus `GetSharingAuthHash` for building\npublic share URLs.\n\n## Contributing \u0026 development\n\n```bash\ndotnet build\ndotnet test\ndotnet format --verify-no-changes\n```\n\nIntegration tests run only when `TESTINGBOT_KEY`/`TESTINGBOT_SECRET` are set and are otherwise skipped.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftestingbot%2Ftestingbot-dotnet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftestingbot%2Ftestingbot-dotnet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftestingbot%2Ftestingbot-dotnet/lists"}