Resources
A tour of the SDK resource namespaces — pentests, RoE, vulnerabilities, evidence, reports, retest, webhooks and more — with realistic method calls.
Every part of the Rank API is exposed as a resource hanging off the client (client.pentests,
client.agents, client.teams, …). Sub-resources nest naturally, for example
client.pentests.vulnerabilities or client.agents.tools. All examples below use the
synchronous client; the asynchronous client has the same shape — just await each call.
The namespaces available on a client are: ai, auth, pentests, teams, agents, chats,
tiers, invitations, permissions, tickets, usage, billing, catalogs, webhooks
(tenant), integrations, report_profiles, remediation_policy and evidence (retention).
Create outbound integrations before you create and launch pentests. Tickets fire from live events; an integration connected after findings already exist does not backfill them.
Pentests
The client.pentests resource covers the full lifecycle: CRUD, execution control and the
nested sub-resources for agents, assets, vulnerabilities and more.
# List (paginated, filterable)
pentests = client.pentests.list(status="running", type="web")
for p in pentests.items:
print(p.id, p.name, p.status)
# Create — at least one asset is required
pentest = client.pentests.create(
name="Web scan",
type="web",
mode="automatic",
assets=[{"asset_type": "url", "asset_value": "https://example.com", "is_primary": True}],
)
# Fail-closed: without an active RoE the orchestrator will not start.
client.pentests.roe.create(
pentest.id,
allowed_domains=["example.com"],
authorization_ref="ENG-2026-0042",
escalation_contacts=[{"type": "email", "value": "secops@example.com"}],
activate=True,
)
# Retrieve, update, delete
pentest = client.pentests.retrieve(pentest.id)
client.pentests.update(pentest.id, name="Web scan v2")
client.pentests.delete(pentest.id)
Lifecycle and control
cancel asks a running stream to stop (Go orchestrator). kill is the terminal
PHP kill switch — evidence is preserved, and a killed pentest cannot resume. They are not
interchangeable.
# Mark a pentest as completed (after phases / processing)
client.pentests.finish(pentest.id)
# Soft stop of a running stream (Go backend)
client.pentests.cancel(pentest.id)
# Kill switch (terminal halt; evidence is preserved)
killed = client.pentests.kill(pentest.id, reason="Out of authorized window")
print(killed.kill_requested, killed.reason)
# Process raw agent output into vulnerabilities with AI (guided mode).
# Processing is asynchronous: this call only enqueues the job and returns
# immediately with status "processing".
client.pentests.process_vulnerabilities(pentest.id, model_alias="gemini-2.5-flash")
# Block until the job reaches a terminal state and read the summary
result = client.pentests.wait_for_vulnerabilities(pentest.id, poll_interval=3.0, timeout=600.0)
print(result.status, result.total_created, result.summary)
# ...or poll the status yourself, without blocking
status = client.pentests.get_vulnerability_status(pentest.id) # processing | completed | failed | idle
# Generate a report, seal it, and email the PDF (pentest must be completed).
# formats: pdf | html | markdown | docx | json — audience: executive | technical | attestation
client.pentests.generate_report(
pentest.id,
recipient_email="security@example.com",
extended=1, # 0 = summary, 1 = full report with evidence
formats=["pdf", "html", "json"],
audience="technical",
)
# Signed evidence manifest (404 until the run is sealed) and hashed control-event trail
client.pentests.manifest(pentest.id)
client.pentests.audit_chain(pentest.id)
client.pentests.verify_audit_chain(pentest.id)
# Retest every eligible resolved finding (409 if a run is already in flight)
client.pentests.retest(pentest.id)
# Narrative operation dump and global quality gate
client.pentests.evidences(pentest.id)
client.pentests.quality_gate(max_open_critical=0, max_open_high=5, min_resolution_rate=80.0)
Launching a pentest and reconnecting to a running one are streaming operations covered in
Streaming: use client.ai.chat.stream(...) to start one and
client.pentests.stream(pentest_id) to reattach. Launching without an active RoE raises
ConflictError (no_active_roe).
Pentest sub-resources
| Sub-resource | Purpose |
|---|---|
client.pentests.vulnerabilities | Triage findings (see below) |
client.pentests.agents | Assign and inspect the agents on a pentest |
client.pentests.assets | Manage targets attached to a pentest |
client.pentests.phases | Inspect the phases of a pentest |
client.pentests.methodologies | List available methodologies |
client.pentests.comments | Read and post pentest comments |
client.pentests.roe | Rules of Engagement (required before launch) |
client.pentests.approvals | Decide in-run human approval gates |
client.pentests.evidence | Sealed artifacts (distinct from evidences()) |
client.pentests.reports | Sealed issued reports (signed download URLs) |
client.pentests.report_settings | Pin this pentest to a report profile |
client.pentests.retests | List, inspect and cancel retest runs |
client.pentests.webhooks | Per-pentest legacy subscriptions |
client.pentests.scheduled | Manage scheduled pentests |
client.pentests.webhooks is the per-pentest legacy surface. Tenant-wide subscriptions that
fire for every pentest of their owner live on client.webhooks.
# Methodologies are needed to create a guided pentest
methodologies = client.pentests.methodologies.list()
for m in methodologies.items:
print(m.id, m.name)
# Assign agents to a phase (guided mode: minimum 3, maximum 4)
client.pentests.agents.assign(
pentest.id,
agents=[
{"agent_id": 10, "phase_id": 1, "execution_order": 1},
{"agent_id": 11, "phase_id": 1, "execution_order": 2},
{"agent_id": 12, "phase_id": 1, "execution_order": 3},
],
)
# Automatic mode auto-assigns the default agents across all phases
defaults = client.pentests.agents.default(pentest.id)
print(f"Auto-assigned {defaults.assigned_count} agents")
Scheduled pentests
Re-run a completed pentest on a recurring schedule with client.pentests.scheduled:
schedule = client.pentests.scheduled.create(
pentest_id=pentest.id,
schedule_type="weekly", # once | daily | weekly | monthly
schedule_time="02:00",
schedule_day="monday",
)
client.pentests.scheduled.list()
client.pentests.scheduled.update(schedule.id, schedule_time="03:30")
client.pentests.scheduled.delete(schedule.id)
Vulnerabilities
Vulnerabilities are nested under a pentest and offer a rich triage workflow. Every method takes
the pentest_id as its first argument.
# List and inspect (taxonomy and validation badges on the row)
vulns = client.pentests.vulnerabilities.list(pentest.id, severity="critical")
for v in vulns.items:
print(v.severity, v.vulnerability, v.status, v.cwe_id, v.validation_status)
# Full details: taxonomy, SLA, provenance-related fields
vuln = client.pentests.vulnerabilities.retrieve(pentest.id, vulnerability_id=42)
print(vuln.cvss_vector, vuln.cwe_id, vuln.validation_status, vuln.due_date)
# Edit text and taxonomy. epss_score / cisa_kev are published signals and
# cannot be written. due_date is computed from the remediation policy.
client.pentests.vulnerabilities.update(
pentest.id, 42,
severity="high",
priority="urgent",
cwe_id="CWE-89",
cvss_vector="CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
severity_override_reason="Business impact exceeds the CVSS rating",
)
# Aggregated stats
client.pentests.vulnerabilities.summary(pentest.id)
# Human review of a finding the validator left inconclusive
client.pentests.vulnerabilities.review(
pentest.id, 42,
validation_status="validated", # or "failed"
reason="Reproduced against staging with the same payload",
)
# Where the finding came from, and whether its evidence digest still matches
prov = client.pentests.vulnerabilities.provenance(pentest.id, 42)
print(prov.evidence_intact, prov.origin)
# JSON envelope, or a document meant for another tool (raw bytes)
client.pentests.vulnerabilities.export(pentest.id, format="json")
sarif = client.pentests.vulnerabilities.export(pentest.id, format="sarif")
open("findings.sarif", "wb").write(sarif.content)
# Enqueue a retest of one resolved finding (409 if a run is already in flight)
client.pentests.vulnerabilities.retest(pentest.id, 42)
Validation statuses: unvalidated | validated | failed | needs_manual_review |
not_validatable | legacy. SARIF, DefectDojo and CSV are vulnerability exports, not
issued-report formats — see Reports.
Status transitions
Each transition is a dedicated method rather than a free-form status write.
client.pentests.vulnerabilities.resolve(
pentest.id, 42, resolution_type="evidenced", comment="Patched in v2.3.1",
)
client.pentests.vulnerabilities.reopen(pentest.id, 42, reason="Regression in v2.3.2")
client.pentests.vulnerabilities.false_positive(pentest.id, 42, reason="Not reachable in prod")
client.pentests.vulnerabilities.accept_risk(pentest.id, 42, reason="Low impact, next quarter")
# Generic transitions are limited to "open" / "in_progress"
client.pentests.vulnerabilities.change_status(pentest.id, 42, status="in_progress")
client.pentests.vulnerabilities.change_priority(pentest.id, 42, priority="urgent")
Assignment, comments, evidence and history
# Assignment to a team member
client.pentests.vulnerabilities.assign(pentest.id, 42, user_id=7, comment="Please investigate")
client.pentests.vulnerabilities.unassign(pentest.id, 42)
# Comments
client.pentests.vulnerabilities.add_comment(pentest.id, 42, comment="Confirmed in staging")
client.pentests.vulnerabilities.list_comments(pentest.id, 42)
client.pentests.vulnerabilities.update_comment(pentest.id, 42, comment_id=15, comment="Updated")
client.pentests.vulnerabilities.delete_comment(pentest.id, 42, comment_id=15)
# Evidence files (httpx multipart tuples)
files = [("files", ("screenshot.png", open("screenshot.png", "rb"), "image/png"))]
client.pentests.vulnerabilities.upload_evidence_files(pentest.id, 42, files=files)
client.pentests.vulnerabilities.list_evidence_files(pentest.id, 42)
client.pentests.vulnerabilities.get_evidence_file(pentest.id, 42, file_id=5)
client.pentests.vulnerabilities.delete_evidence_file(pentest.id, 42, file_id=5)
# Original operation evidence and change history
client.pentests.vulnerabilities.evidence(pentest.id, 42)
client.pentests.vulnerabilities.history(pentest.id, 42)
Bulk and quality gate
# Bulk status change (targets limited to "open" / "in_progress")
result = client.pentests.vulnerabilities.bulk_update_status(
pentest.id, vulnerability_ids=[42, 43, 44], status="in_progress", comment="Batch triage",
)
# Per-pentest quality gate
client.pentests.vulnerabilities.quality_gate(
pentest.id,
rules=[
{"severity": "critical", "max_open": 0},
{"severity": "high", "max_open": 5},
{"min_resolution_rate": 0.8},
],
)
# Global summary across all pentests
client.pentests.vulnerabilities.global_summary()
Engagement
The run is fail-closed. At least one of allowed_cidrs or allowed_domains is required, and
every pentest asset must fall inside that allow-list. A matching deny-list entry always
wins. See Engagement, RoE & kill switch.
# Vocabulary of actions that can wait for a human
classes = client.catalogs.list_approval_classes()
for c in classes.items:
print(c.id, c.description)
current = client.pentests.roe.retrieve(pentest.id)
if current.active:
print(current.active.version, current.active.status)
roe = client.pentests.roe.create(
pentest.id,
allowed_domains=["example.com"],
allowed_cidrs=["93.184.216.34/32"],
denied_cidrs=["10.0.0.0/8"],
timezone="Europe/Madrid",
max_rps=10,
max_concurrency=4,
requires_approval_for=["exploit"],
auto_approve=False,
authorization_ref="ENG-2026-0042",
escalation_contacts=[{"type": "email", "value": "secops@example.com"}],
activate=True,
)
print(roe.status, roe.version)
# PUT merges onto the current version; pass activate=True again to cut over
client.pentests.roe.update(pentest.id, max_rps=5, activate=True)
# Restricted actions pause until you decide them
pending = client.pentests.approvals.list(pentest.id, status="pending")
for a in pending.approvals:
client.pentests.approvals.decide(
pentest.id, a.id,
decision="approve", # or "deny"
reason="In scope for this window",
)
# Emergency stop — distinct from cancel()
client.pentests.kill(pentest.id, reason="Out of authorized window")
Catalogs
Taxonomy and report-profile vocabularies used when mapping findings or shaping a report.
cwes = client.catalogs.list_cwe(q="injection", mapping="allowed")
for entry in cwes.items:
print(entry.cwe_id, entry.name)
cwe = client.catalogs.retrieve_cwe("CWE-89")
print(cwe.name, cwe.description)
tech = client.catalogs.list_attack_techniques(tactic="Initial Access")
one = client.catalogs.retrieve_attack_technique("T1190")
fields = client.catalogs.list_report_profile_fields()
for field in fields.fields:
print(field.key, [opt.id for opt in field.options])
Evidence
client.pentests.evidences() is the narrative dump of operations. Sealed artifacts live
under client.pentests.evidence. Retention policy lives on client.evidence (not on the
pentest).
# Sealed artifacts (optional filter by finding)
artifacts = client.pentests.evidence.list(pentest.id, vulnerability_id=42)
one = client.pentests.evidence.retrieve(pentest.id, artifacts.items[0].id)
print(one.url) # time-limited; never inline for large files
# Signed manifest (404 until the run is sealed)
manifest = client.pentests.manifest(pentest.id)
print(manifest.merkle_root, manifest.signature_valid)
# Hashed control-event trail
chain = client.pentests.audit_chain(pentest.id, event_type="kill_requested")
print(client.pentests.verify_audit_chain(pentest.id).valid)
# How long artifacts are kept (omit team_id for personal policy)
policy = client.evidence.get_retention_policy()
client.evidence.set_retention_policy(retention_days=365, credential_retention_days=30)
Reports
Configure the tenant (or team) profile, optionally pin a pentest to it, then generate. The
pentest must be completed (status_code 5). Override formats and audience on a single
emission without changing the stored profile.
profile = client.report_profiles.retrieve()
client.report_profiles.update(audience="technical", default_formats=["pdf", "html"])
# Pin this pentest to a profile, or None for the tenant default
client.pentests.report_settings.update(pentest.id, report_profile_id=profile.profile.id)
settings = client.pentests.report_settings.retrieve(pentest.id)
print(settings.effective.audience, settings.effective.default_formats)
result = client.pentests.generate_report(
pentest.id,
recipient_email="security@example.com",
recipient_name="Security Team",
subject="Pentest Report — example.com",
cc_emails=["cto@example.com"],
extended=1,
formats=["pdf", "html", "json"], # pdf | html | markdown | docx | json
audience="technical", # executive | technical | attestation
)
print(result.message, result.sealed, result.emailed)
# Sealed files are listed separately (signed URL, never inline)
reports = client.pentests.reports.list(pentest.id)
for r in reports.items:
print(r.format, r.payload_sha256, r.sealed_at)
download = client.pentests.reports.retrieve(pentest.id, reports.items[0].id)
print(download.url)
# Team default (owner only)
client.teams.report_profiles.retrieve(team.id)
client.teams.report_profiles.update(team.id, audience="executive")
Retest
Retest results: pending | fixed | still_vulnerable | inconclusive | skipped. Auth
context (cookies / headers) is accepted on enqueue and never returned.
# One finding (409 if a run is already in flight)
run = client.pentests.vulnerabilities.retest(pentest.id, 42)
# Every eligible resolved finding on the pentest
batch = client.pentests.retest(pentest.id)
# Compact list, then detail (findings + replay evidence)
runs = client.pentests.retests.list(pentest.id)
detail = client.pentests.retests.retrieve(pentest.id, runs.items[0].id)
client.pentests.retests.cancel(pentest.id, runs.items[0].id) # only while queued
# Tenant SLA / auto-retest defaults
policy = client.remediation_policy.retrieve()
client.remediation_policy.update(
sla_hours={"critical": 24, "high": 72},
auto_retest_on_resolve=True,
auto_retest_on_ticket_close=True,
)
# Team overrides (owner only)
client.teams.remediation_policy.retrieve(team.id)
client.teams.remediation_policy.update(team.id, retest_sla_hours=48)
Tenant webhooks
client.webhooks fires for every pentest of its owner. Per-pentest subscriptions remain
at client.pentests.webhooks. Verify deliveries with rank.verify_signature — it lives on
the package, not on the client. See Client setup.
# Secret is returned once
hook = client.webhooks.create(
url="https://example.com/rank/hooks",
events=["vulnerability.validated", "pentest.killed", "control.approval_requested"],
description="SOC inbox",
)
print(hook.secret)
client.webhooks.test(hook.webhook.id)
deliveries = client.webhooks.list_deliveries(hook.webhook.id)
for d in deliveries.items:
print(d.event_type, d.status, d.http_status)
# HMAC-SHA256 of "{timestamp}.{raw_body}" (v1=, 5-minute window)
payload = request_body_bytes # raw body — do not re-serialize JSON
headers = request_headers # must include X-Rank-Signature and X-Rank-Timestamp
rank.verify_signature(payload, headers, secret="whsec_...")
Integrations
Providers available today: jira, github, slack. Create the integration before you
create and launch pentests so new findings open tickets. Connecting after findings already
exist does not backfill those events — call sync to push a pentest after the fact.
providers = client.integrations.providers()
for p in providers.providers:
print(p.provider, p.label)
integ = client.integrations.create(
provider="jira",
name="SecOps Jira",
credentials={"email": "bot@example.com", "api_token": "..."},
config={"project_key": "SEC"},
events=["vulnerability.validated"],
sync_inbound=True,
)
client.integrations.test(integ.integration.id)
# Backfill a pentest that already has findings
client.integrations.sync(integ.integration.id, pentest_id=pentest.id)
client.integrations.links(integ.integration.id)
Agents
Agents are the AI workers that run pentests and power chat. The client.agents resource manages
them, their tools, the AI models they run on, and the MCP servers they connect to.
# List agents (filterable by type and phase)
client.agents.list(type="pentest")
client.agents.list(type="pentest", phase_id=1)
client.agents.list(type="general")
# Create, update, clone, delete
agent = client.agents.create(
name="Custom Recon Agent",
instructions="Enumerate subdomains, scan ports and fingerprint technologies.",
agent_type="pentest",
phase_id=1,
model_id=6,
effort="high", # optional reasoning-effort override
thinking_enabled=True, # optional thinking toggle
)
client.agents.update(agent.agent.id, name="Recon Agent v2")
client.agents.clone(agent.agent.id)
client.agents.delete(agent.agent.id)
Reasoning: effort and thinking
create and update take two optional reasoning controls — effort (a ReasoningEffort
literal: "none", "minimal", "low", "medium", "high", "xhigh", "max") and
thinking_enabled (a bool). Omit them to inherit the model’s defaults; the API validates both
against the target model. See Agents, tools & MCP.
# Tune reasoning on an existing agent
client.agents.update(agent_id=10, effort="max", thinking_enabled=True)
# Inspect the current configuration and the model capabilities that bound it
detail = client.agents.retrieve(10)
print(detail.effort, detail.thinking_enabled) # the overrides (None = inherited)
print(detail.default_effort, detail.effort_values) # what the model allows
print(detail.supports_effort, detail.supports_thinking_toggle)
# Discover valid values before configuring: read them from the model
model = client.agents.models.retrieve(model_id=6)
if model.supports_effort:
print("valid effort levels:", model.effort_values, "default:", model.default_effort)
Models, tools and MCP servers
# AI models — list the catalog, assign to your account, then use them on agents.
# Each model carries its reasoning capabilities: reasoning, supports_effort,
# effort_values, default_effort and supports_thinking_toggle.
client.agents.models.list()
client.agents.models.assign(model_ids=[5, 6, 12])
client.agents.models.mine()
client.agents.models.remove(model_id=5)
# Tools assigned to an agent
client.agents.tools.list(agent_id=10)
client.agents.tools.available(agent_id=10)
client.agents.tools.assign(agent_id=10, tool_ids=[1, 2, 3])
client.agents.tools.remove(agent_id=10, tool_id=3)
# MCP servers
server = client.agents.mcps.create(
name="Internal Tooling MCP", transport_type="streamable_http", url="https://mcp.internal.example.com",
)
client.agents.mcps.assign(agent_id=10, mcp_server_ids=[server.id])
client.agents.mcps.list(agent_id=10)
Teams
Teams are the collaboration layer. Roles and permissions gate access to shared pentests, agents and chats, so set those up before inviting members.
team = client.teams.create(name="Security Team", description="Red team operations")
# Roles and permissions
role = client.teams.roles.create(team.id, role_name="Pentester")
client.teams.roles.add_permissions(team.id, role.id, permission_ids=[1, 2, 3])
# Invitations and members
client.teams.invitations.create(team.id, email="pentester@example.com", role_id=role.id)
client.teams.members.list(team.id)
client.teams.roles.assign_to_member(team.id, role.id, user_id=5)
# Shared resources and usage
client.teams.agents.create(team.id, agent_id=10)
client.teams.usage.summary(team.id)
The platform-wide permission catalog used to build roles lives on client.permissions.list().
Chats
A chat session provides persistence and conversation context for AI interactions, including the phases of a pentest.
chat = client.chats.create(nombre="Security research")
client.chats.retrieve(chat.id)
client.chats.update(chat.id, nombre="Renamed session")
client.chats.list()
client.chats.mine()
client.chats.shared()
# Archive / share lifecycle
client.chats.archive(chat_id=chat.id)
client.chats.unarchive(chat_id=chat.id)
client.chats.share(chat_id=chat.id, team_id=4)
client.chats.unshare(chat_id=chat.id, team_id=4)
client.chats.unshare_all(chat_id=chat.id)
# Operations (message history) and operation logs
client.chats.operations.list(chat_id=chat.id)
client.chats.operations.assign(chat_id=chat.id, operations=[101, 102])
client.chats.operations.delete(chat_id=chat.id, operation_id=101)
client.chats.operation_logs.list()
client.chats.delete(chat.id)
The chat resource manages the conversation container; the messages themselves are produced over the streaming backend (see Streaming). The matching HTTP routes are in the Chats API.
Usage and billing
Monitor consumption and control spend.
# Current billing period, or filter by month / date range
client.usage.summary()
client.usage.summary(month="2026-06")
# Daily and hourly breakdowns, and the full operation history
client.usage.daily(period="7d")
client.usage.daily(date="2026-06-20")
client.usage.history(page=1, per_page=50)
# On-demand spending
client.usage.toggle_on_demand(enabled=True, limit_usd=50.0)
# Subscription, trial and invoices (personal)
client.billing.status()
client.billing.trial_status()
client.billing.invoices()
# Team billing (read-only views)
client.billing.team_status(team_id=4)
client.billing.team_trial_status(team_id=4)
client.billing.team_invoices(team_id=4)
client.tiers.list()
Auth and account
# The authenticated user and profile
client.auth.me()
client.auth.update_profile(country="ES", language="es")
# Account changes
client.auth.update_email(email="new@example.com")
client.auth.update_username(username="new-handle")
client.auth.update_password(current_password="...", new_password="...")
# Sessions
client.auth.sessions()
client.auth.active_sessions()
client.auth.revoke_session(session_id=88)
# API tokens — programmatic management
client.auth.api_tokens.available_permissions()
client.auth.api_tokens.list()
created = client.auth.api_tokens.create(
name="CI/CD pipeline", permission_ids=[12, 13], tags=["pentest"], team_ids=[4],
)
print(created.token) # plaintext rk_... value — shown only once
client.auth.api_tokens.update(token_id=created.id, name="CI/CD pipeline v2")
client.auth.api_tokens.revoke(token_id=created.id)
The full HTTP surface, including two-factor authentication, is documented in the Authentication & account API.
Tickets
Push findings to Jira through the client.tickets resource. A Jira integration must be configured for your account or team first.
ticket = client.tickets.create(
summary="XSS in login form",
description="Reflected XSS via the redirect_url parameter",
issue_type="Bug",
)
client.tickets.list()
client.tickets.retrieve(ticket.id)
# Comments and attachments
client.tickets.add_comment(ticket.id, comment="Confirmed in staging")
client.tickets.update_comment(ticket.id, comment_id="10001", comment="Confirmed in prod too")
client.tickets.delete_comment(ticket.id, comment_id="10001")
client.tickets.add_attachment(ticket.id, file=("poc.png", open("poc.png", "rb"), "image/png"))
# Pull the latest state from Jira, then delete
client.tickets.sync(ticket.id)
client.tickets.delete(ticket.id)
The matching HTTP routes are in the Tickets API.
For real-time work — launching pentests, following agent activity and chatting with general agents — head to Streaming. For typed errors and paging through list responses, see Errors & pagination.
Create and activate a RoE, list approvals, decide them, and fire the kill switch.
Evidence & auditSealed artifacts, the signed manifest and the hashed control-event chain.
Reports APIProfiles, pentest overrides, issued reports and signed downloads.
Retest & remediationEnqueue a retest, inspect runs and set SLA / auto-retest policy.
WebhooksTenant subscriptions, signed deliveries and the event catalog.
IntegrationsJira, GitHub and Slack — create before you launch.