Skip to main content

Official Python SDK for ReplyLayer — email for AI agents

Project description

replylayer

Official Python SDK for ReplyLayer — secure email for AI agents.

Looking for the command-line tool? This package is the SDK library (import replylayer). For the rly / replylayer CLI, install rly instead: pipx install rly.

Install

pip install replylayer

Quick start

from replylayer import ReplyLayer

rl = ReplyLayer(api_key="rly_live_k3m9p2qx7vn4hjd0.uZ8Qb1vK3mN0pR7sT2wX9yA4cF6gH8jL1nP3rT5vW7z")

# Create a mailbox
mailbox = rl.mailboxes.create(name="support")

# Send an email
sent = rl.messages.send(
    from_mailbox=mailbox["name"],
    to="user@example.com",
    subject="Hello from my agent",
    body="Hi there!",
)

# Wait for a reply (long-poll, up to 30s)
result = rl.messages.wait(mailbox["id"])
if result["message"]:
    msg = result["message"]
    print(f"Got reply from {msg['sender']}: {msg['subject']}")

# Browse conversation threads
page = rl.threads.list(mailbox["id"])
for thread in page.data:
    print(f"{thread['subject']} ({thread['message_count']} messages)")

Async usage

from replylayer import AsyncReplyLayer

async with AsyncReplyLayer(api_key="rly_live_k3m9p2qx7vn4hjd0.uZ8Qb1vK3mN0pR7sT2wX9yA4cF6gH8jL1nP3rT5vW7z") as rl:
    mailbox = await rl.mailboxes.create(name="support")
    sent = await rl.messages.send(
        from_mailbox=mailbox["name"],
        to="user@example.com",
        subject="Hello",
        body="Hi!",
    )

Constructor options

ReplyLayer(
    api_key="rly_live_k3m9p2qx7vn4hjd0.uZ8Qb1vK3mN0pR7sT2wX9yA4cF6gH8jL1nP3rT5vW7z",  # required
    base_url="https://api.replylayer.ai",       # default
    max_retries=3,                              # retries on 429/5xx (0 = fail-fast)
    timeout=30.0,                               # seconds per request
    max_retry_after_seconds=4000.0,             # cap on honoring a 429 Retry-After (~67min)
    on_retry=None,                              # silent-by-default retry hook
)

Retry behavior

The client retries failed requests up to max_retries times (default 3). The contract — read it before relying on retries:

  • 429 is retried on every method, including mutating ones (POST / PATCH / DELETE). A 429 is a pre-dispatch rate-limit rejection, so nothing happened server-side — retrying is safe. The wait honors the Retry-After header.
  • 5xx is retried only on non-mutating (GET) requests. A 5xx on a POST / PATCH / DELETE is not retried — the request may have executed, so a retry risks a double-send (or, for DELETE, retrying a lost-but-applied delete into a confusing 404).
  • Multipart uploads are never retried (a retry would re-send the body).
  • Long Retry-After values block up to max_retry_after_seconds (default ~67 minutes, sized to ride out hour-bucket rate limits for batch jobs). When a server Retry-After exceeds this cap, the SDK raises the RateLimitError rather than sleeping — it never clamps-and-retries into a still-limited window. Interactive callers should set a low cap (e.g. max_retry_after_seconds=30).
  • max_retries=0 is fail-fast — no implicit retry of any kind. Recommended for interactive / agent contexts. Branch on RateLimitError.retry_after.
  • on_retry is silent by default — the SDK never writes to stdout/stderr. Pass an on_retry(info) hook to log or meter retries; it receives a RetryInfo (attempt, error, delay_seconds, method, path). On the async client it may be a coroutine (it's awaited); a raising callback is swallowed so it can't break the retry.

Resources

Resource Methods
rl.mailboxes create, list, delete, update, set_recipient_policy
rl.mailboxes.allowlist list, add, add_bulk, delete, list_blocked_attempts
rl.messages send, list, get, reply, wait, release, block, set_starred
rl.drafts create, get, list, update, send, delete
rl.threads list, get, set_starred
rl.attachments get_download_url, get_preview, upload, get_upload, delete_upload
rl.webhooks create, list, get, update, delete, rotate_secret, test, list_deliveries, retry_delivery
rl.recipients create, list, delete, resend
rl.suppressions list, delete
rl.api_keys create, list, revoke, rotate*
rl.account get_usage
rl.health check

*api_keys.rotate() revokes the calling API key and returns a new one. After calling it, this SDK instance's key is invalidated — create a new ReplyLayer instance with the returned key.

Drafts: scan-then-review-then-send

rl.drafts.create() runs the outbound scanner synchronously and attaches the verdict to the draft. The create-time verdict is UX — it lets an agent (or a human approver) see the likely outcome before clicking send. rl.drafts.send() re-runs the scanner authoritatively against the mailbox's current policy, so a stale cached verdict cannot slip through.

draft = rl.drafts.create(
    mailbox_id=mailbox["name"],
    to="user@example.com",
    subject="Re: your invoice",
    body="Thanks for your question.",
)
if draft["worst_decision"] == "allow":
    result = rl.drafts.send(draft["id"])
    print(f"Sent {result['message_id']}")

The send/reply/draft-send response carries two additive, nullable keys that explain a held send inline (no second messages.get call). result["scan"] is the vendor-neutral scanner verdict (ScanSummary); result["hold_context"] ({"trigger_source", "summary_reasons"} or None) is the policy/HITL reason, non-null only when the delivery status diverges from scan["verdict"] because of a policy/HITL hold — a clean scan held for review by your mailbox policy, or a scanner review-flag held as quarantine on a plan without the review queue (trigger_source: mailbox_policy | scanner | both).

By default drafts.send(), messages.send(), and messages.reply() return only once the scanner verdict is known, with scan and hold_context inline. Pass async_dispatch=True to drafts.send() to send the Prefer: respond-async hint. The hint is advisory — the server returns a 202 AsyncSendAck only when OUTBOUND_ASYNC_DISPATCH_ENABLED is on; otherwise it ignores the hint and returns a normal SendMessageResponse. Always branch on the result: result["status"] == "queued_for_dispatch"AsyncSendAck, otherwise SendMessageResponse. Poll messages.get(message_id) (or handle the lifecycle webhook) until state is terminal. Attachment-bearing drafts fail closed on the async path (400 ATTACHMENTS_REQUIRE_SYNC_SEND). (messages.wait() is a mailbox long-poll for new inbound mail, not a way to observe a specific message by ID.)

The send endpoint raises ReplyLayerError with distinct .code values on 409:

  • DRAFT_REJECTED_BY_RESCAN — send-time scan flipped the verdict to block/quarantine. The draft stays in draft state; edit the body and retry. err.details carries scan and, when a policy/HITL decision drove the hold, hold_context.
  • DRAFT_ALREADY_SENT — the draft was already sent (race or retry after success).
from replylayer import ReplyLayerError

try:
    rl.drafts.send(draft["id"])
except ReplyLayerError as err:
    if err.code == "DRAFT_REJECTED_BY_RESCAN":
        print("Rescan blocked it:", err.details)

Outbound attachments (Pro+)

Attaching a file is a two-phase flow: upload the bytes to stage a handle, then reference handle["id"] in a send/reply/draft attachment_ids list. Every attachment is scanned (byte-level family validation + AV + secrets/PII over extracted text and filename) before it leaves. The mailbox must have outbound attachments explicitly enabled (a Pro+, session-gated dashboard action) — uploads to a non-enabled mailbox raise ForbiddenError with code="OUTBOUND_ATTACHMENTS_DISABLED".

import time

# Phase 1 — stage the file (returns an opaque handle).
with open("invoice.pdf", "rb") as f:
    handle = rl.attachments.upload(
        mailbox_id="support",
        file=f.read(),                  # bytes or a file-like object
        filename="invoice.pdf",
        content_type="application/pdf",  # advisory — the server re-sniffs the bytes
    )

# The content scan runs asynchronously. Poll until it leaves "pending".
status = handle["content_scan_status"]   # "pending" at upload time
while status == "pending":
    time.sleep(1)
    polled = rl.attachments.get_upload(handle["id"])
    if polled.get("status") == "consumed":
        break
    status = polled["content_scan_status"]

# Phase 2 — reference the handle on a send. "clean" and "flagged" both send
# (a "flagged" finding flows to the message verdict, like a body finding);
# "error" is fail-closed.
result = rl.messages.send(
    from_mailbox="support",
    to="user@example.com",
    subject="Your invoice",
    body="Attached.",
    attachment_ids=[handle["id"]],
)

A handle is consumed once at send and is single-mailbox-scoped (upload to the same mailbox you send from). Unconsumed handles expire after 24h; delete one early with rl.attachments.delete_upload(handle["id"]). Limits: 10 MB/file, 10 attachments and 15 MB total per message. Image attachments require a separate one-time image-risk disclaimer on the mailbox (OUTBOUND_IMAGE_DISCLAIMER_REQUIRED). Drafts hold handles and consume them at dispatch; rl.drafts.update(draft_id, attachment_ids=None) clears a draft's attachments. Attachment bytes are stored with provider-managed encryption-at-rest and transmitted over TLS — this is not end-to-end / zero-access encryption (the platform scans attachment content).

Delivery history & manual retry

rl.webhooks.list_deliveries(id, limit=..., before_at=..., before_id=...) returns the most recent delivery attempts for a webhook with tuple-cursor keyset pagination. before_at and before_id must be provided together — the SDK omits the cursor entirely if only one is given.

page = rl.webhooks.list_deliveries(webhook_id, limit=50)
while page["has_more"]:
    page = rl.webhooks.list_deliveries(
        webhook_id,
        limit=50,
        before_at=page["next_before_at"],
        before_id=page["next_before_id"],
    )

rl.webhooks.retry_delivery(webhook_id, delivery_id) re-queues a single failed delivery. The API rejects retries on non-failed deliveries or deliveries whose parent webhook is disabled — surfaced as ReplyLayerError with .code set to DELIVERY_NOT_FAILED or WEBHOOK_DISABLED:

from replylayer import ReplyLayer, ReplyLayerError

try:
    rl.webhooks.retry_delivery(webhook_id, delivery_id)
except ReplyLayerError as err:
    if err.code == "WEBHOOK_DISABLED":
        # Resume the webhook (PATCH enabled=True) before retrying.
        pass

Mailbox settings

Each mailbox carries a scanner policy and a PII delivery mode:

# Redact PII before delivering inbound bodies to the agent
rl.mailboxes.update(
    mailbox["id"],
    scanner_policy={"language_mode": "english_only"},
    pii_mode="redacted",
)

pii_mode values:

  • "passthrough" (default) — message reads return body.content as a plaintext display projection. Session-cookie dashboard callers can opt into sanitized HTML with body_format=html.
  • "redacted"body.content is plaintext with detected PII spans replaced by <TYPE> tags (e.g. <EMAIL_ADDRESS>, <PHONE_NUMBER>). Requires Starter tier or above; sandbox accounts get 403 TIER_LIMIT.

pii_mode="redacted" also applies to outbound webhook payloads: message.* events have sender/recipient/to<EMAIL_ADDRESS> and subject<REDACTED> before signing. The HMAC covers the redacted body — verify_webhook_signature works without any client-side changes.

Advanced PII config (Pro+)

PR 8 added pii_redaction_config for per-detector control over redaction (e.g. "leave email visible, redact everything else") and operator-level rendering (partial_mask for credit cards, hash_replace for emails you want to dedupe without exposing). Pro+ feature; only meaningful when pii_mode="redacted".

# Per-detector toggle: show emails to the agent, keep everything else redacted.
rl.mailboxes.update(
    mailbox["id"],
    pii_mode="redacted",
    pii_redaction_config={
        "EMAIL_ADDRESS": {"redact": False},
    },
)

# partial_mask: render credit cards as ****-****-****-1111 (separators preserved).
# `keep_last` is 1-6; `mask_char` defaults to "*".
rl.mailboxes.update(
    mailbox["id"],
    pii_redaction_config={
        "CREDIT_CARD": {
            "redact": True,
            "operator": {"kind": "partial_mask", "keep_last": 4},
        },
    },
)

# hash_replace: <EMAIL_ADDRESS:a3f1b9c2>. Deterministic per account; opaque
# across accounts. Lets your agent dedupe without seeing raw values.
rl.mailboxes.update(
    mailbox["id"],
    pii_redaction_config={
        "EMAIL_ADDRESS": {
            "redact": True,
            "operator": {"kind": "hash_replace"},
        },
    },
)

# Reset to platform default.
rl.mailboxes.update(mailbox["id"], pii_redaction_config={})

Tier gate. Any non-default value (a redact: False entry OR an operator with kind: "partial_mask" or kind: "hash_replace") requires the pii_advanced_controls feature (Pro+). Non-feature accounts can still PATCH {}, default-shape entries ({"redact": True}, {"kind": "replace_with_type"}).

partial_mask whitelist. PERSON and EMAIL_ADDRESS are rejected (422) — partial-masking a name produces nonsense; partial-masking an email is hard to do well in v1. Use hash_replace for those instead.

Downgrade behavior. If you configure non-default pii_redaction_config on Pro and then downgrade, the persisted JSONB stays on the row but the read-side IGNORES it. Reads fall back to platform default. Re-upgrading restores the config instantly. The dashboard surfaces a "Saved but inactive" banner in this state.

Webhook scope-out. Advanced PII config does NOT apply to webhook payload metadata. Webhook delivery continues to use pii_mode for envelope-level field redaction; per-detector and operator control is API read-side only.

The Python SDK ships static type hints for PiiOperator (a Union of PiiReplaceWithTypeOperator, PiiPartialMaskOperator, and PiiHashReplaceOperator TypedDicts) — so a config like {"kind": "hash_replace", "keep_last": 4} is caught by mypy / pyright at the SDK boundary, not just at the server's 422.

Outbound PII send safety. ScannerPolicy.outbound_pii_policy tunes send decisions for the local outbound PII scanner by type:

rl.mailboxes.update(
    mailbox["id"],
    scanner_policy={
        "outbound_pii_policy": {
            "ssn": "quarantine",
            "credit_card": "review",
            "phone_number": "allow_with_warning",
        },
        "outbound_review_policy": {
            "approval_note": "required_for_sensitive_pii",
        },
    },
)

Supported actions are "allow", "allow_with_warning", "review", "quarantine", and "block". "review" routes matching sends to Pending approval; enabling it requires both Pro+ outbound PII controls and the review queue feature. Relaxing below platform defaults requires Pro+ (pii_advanced_controls); default or stricter values are accepted on every tier. Outbound PII scan results include pii_type ("ssn", "credit_card", or "phone_number") so clients can inspect which type drove the action.

Approval notes are optional by default. Set outbound_review_policy.approval_note to "required_for_sensitive_pii" when approvers must add a note before sending SSN or credit-card review holds.

Agent Attachment Access

Effective attachment exposure now comes from the mailbox policy surface (attachment_exposure_mode plus attachment_allowed_file_families), not from the legacy attachment_access_enabled boolean alone. Admin keys, pre-scoping keys, and dashboard sessions still bypass the agent mailbox-policy gate. Agent-key download requests without an explicit raw-download policy return 403 ATTACHMENT_ACCESS_DISABLED — surfaced as ReplyLayerError with .code == "ATTACHMENT_ACCESS_DISABLED":

from replylayer import ReplyLayerError

try:
    rl.attachments.get_download_url(message_id, 0)
except ReplyLayerError as err:
    if err.code == "ATTACHMENT_ACCESS_DISABLED":
        # Admin can configure the mailbox attachment policy through the
        # dashboard or POST /v1/mailboxes/:id/attachment-access.
        ...

Explicit raw_download_selected_types enablement requires a Pro+ production account, session-cookie auth, and fresh TOTP/password re-auth, so Bearer-key SDK clients receive 403 REAUTH_REQUIRES_SESSION when they try to enable or widen approved downloads. The SDK can still read attachment policy state, disable raw downloads, set metadata_only / derived_content, and perform same-or-narrower writes on an already-explicit approved-download mailbox.

Images are a separately confirmed raw-download family. When allowed_file_families includes "image", pass accept_image_risk_version matching the mailbox response's current_image_risk_version unless the mailbox already has current image-risk acceptance. A mailbox response reports image state with image_raw_download_confirmed, attachment_image_access_accepted_at, and attachment_image_access_accepted_version. Legacy wildcard rows and stale image acceptances do not grant raw image downloads.

Human dashboard sessions and admin/pre-scoping keys can download clean stored metadata_only attachments, including attachments held back from agent raw-download policy. Agent-role keys remain bound to the mailbox policy gate plus hard safety checks; all callers remain blocked by infected AV verdicts, non-terminal message states, missing stored bytes, and hard attachment blocks.

See ENDPOINTS.md for the full contract and known limitations.

Recipient allowlist (mailbox containment)

A mailbox is in blocklist mode by default — the pre-send gate rejects suppressed_addresses hits and allows everyone else. Switching to allowlist mode restricts outbound to a pre-approved list; an agent (or a compromised API key) physically cannot email outside the list.

# Populate the allowlist first. Admin-only — agent keys get 403 INSUFFICIENT_SCOPE.
rl.mailboxes.allowlist.add(mailbox["id"], email="partner@corp.com")
rl.mailboxes.allowlist.add_bulk(mailbox["id"], emails=["a@x.com", "b@y.com"])

# Flip the mode. Returns 400 ALLOWLIST_EMPTY if the list is empty unless
# force_empty=True is passed to acknowledge the lockout.
rl.mailboxes.set_recipient_policy(mailbox["id"], "allowlist")

# Sends to off-list recipients now 403 with code RECIPIENT_NOT_ON_ALLOWLIST.
# Blocklist still runs first — a recipient on the do-not-contact list is
# rejected 403 with code RECIPIENT_SUPPRESSED (details["reason"] == "suppressed").

# Deleting the last entry while in allowlist mode returns 409 ALLOWLIST_LAST_ENTRY;
# pass force_empty=True to acknowledge.
rl.mailboxes.allowlist.delete(mailbox["id"], "partner@corp.com", force_empty=True)

A send/reply/draft-send to a recipient on your do-not-contact (suppression) list raises ReplyLayerError with .code == "RECIPIENT_SUPPRESSED" (HTTP 403, details["reason"] == "suppressed"). This is terminal — escalate, don't retry; remove the suppression or send to a different recipient.

Allowlist mutations are admin-only — granting send permission to an LLM defeats the containment boundary. Agents can list (so they can see what they're allowed to email) but not add/add_bulk/delete. Three new webhook events: recipient_allowlist.added, recipient_allowlist.removed, mailbox.recipient_policy_changed.

Domain entries (sprint 039)

Entries can be either an exact email (alice@corp.com) or a bare-domain pattern (@corp.com) that matches every address at that domain. Exact-domain only — @corp.com matches *@corp.com but NOT eve@sub.corp.com.

# Allow everyone at @partner.com.
rl.mailboxes.allowlist.add(mailbox["id"], email="@partner.com")

# Block a whole competitor domain.
rl.suppressions.add(email="@competitor.com")

# Bulk mix emails + domains.
bulk = rl.mailboxes.allowlist.add_bulk(
    mailbox["id"],
    emails=["alice@corp.com", "@partner.com", "not-an-email"],
)
# bulk["added"][0]["pattern_type"] == "email"
# bulk["added"][1]["pattern_type"] == "domain"
# bulk["invalid"][0]                    == {"email": "not-an-email", "reason": "invalid_format"}

Responses expose pattern_type: "email" | "domain" on every add/list/delete/bulk-added row. Pre-0.5.0 servers omit the field.

Blocklist precedence still holds: a domain-block beats an exact-allow at the same domain. Malformed patterns (@, @.com, @foo, @corp-.com, non-ASCII) raise ReplyLayerError with .code == "INVALID_EMAIL" (message: "Invalid email or domain pattern").

Blocked attempts (migration 038)

Every send the allowlist gate rejects writes an append-only audit row and emits a deduped recipient_allowlist.blocked_attempt webhook. Review the log to see what your agent tried to email and one-click add legitimate recipients.

# Aggregated top-N view — grouped by (recipient, actor_id).
# next_cursor is always None; the aggregate is top-N, not paginated.
result = rl.mailboxes.allowlist.list_blocked_attempts(mailbox["id"])
for a in result["attempts"]:
    print(f"{a['recipient']} × {a['count']} (last: {a['last_attempted_at']})")

# "Blocked this week" — recency filter (1..365 days).
week = rl.mailboxes.allowlist.list_blocked_attempts(mailbox["id"], within_days=7)

# Raw per-attempt history for forensic drill-in. Paginates via tuple cursor.
raw = rl.mailboxes.allowlist.list_blocked_attempts(
    mailbox["id"], aggregate=False, limit=100,
)

Async parity is identical — await rl.mailboxes.allowlist.list_blocked_attempts(...).

Webhook deliveries are deduped server-side to at most one per (account, mailbox, recipient) per 60 seconds — a looping agent produces one delivery, not hundreds, keeping your subscription below the 20-abandoned-deliveries auto-disable threshold. Full attempt history is always available via list_blocked_attempts.

The MCP tool list_allowlist_blocked_attempts exposes the same view to agents — read-only by design. There is no dismiss-attempt tool (the containment boundary would be moot if an agent could clear its own rejection history).

Mailbox identifiers

Every SDK method that takes a mailbox_id argument accepts either the mailbox's UUID or its name. The server resolves names against the authenticated account's active mailboxes. rl.messages.list("support-bot") and rl.messages.list("a1b2-…") are equivalent.

Pagination

List endpoints return a Page with data, has_more, and cursor:

page = rl.messages.list("mailbox-id", limit=50)
print(page.data)      # list of message dicts
print(page.has_more)  # bool
print(page.cursor)    # str | None

Pass auto_paginate=True for an iterator:

for msg in rl.messages.list("mailbox-id", auto_paginate=True):
    print(msg["subject"])

# Async
async for msg in rl.messages.list("mailbox-id", auto_paginate=True):
    print(msg["subject"])

Error handling

from replylayer import ReplyLayer
from replylayer.errors import NotFoundError, RateLimitError

try:
    rl.messages.get("nonexistent")
except NotFoundError:
    print("Message not found")
except RateLimitError as e:
    print(f"Rate limited, retry after {e.retry_after}s")

Error classes: ReplyLayerError (base), AuthenticationError (401), ForbiddenError (403), NotFoundError (404), ValidationError (400/422), RateLimitError (429).

Webhook signature verification

For a full integration guide (event catalog, retry behavior, idempotency, security, troubleshooting), see docs/webhooks.md.

from replylayer import verify_webhook_signature

verify_webhook_signature(
    payload=request.body,
    signature=request.headers["x-replylayer-signature"],
    secret="whsec_...",
    tolerance=300,  # optional, seconds (default 300)
)

Context managers

Both clients support context managers to properly close connection pools:

with ReplyLayer(api_key="...") as rl:
    rl.messages.send(...)
# connections closed

async with AsyncReplyLayer(api_key="...") as rl:
    await rl.messages.send(...)

Requirements

  • Python >= 3.10
  • httpx >= 0.27

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

replylayer-0.16.0.tar.gz (81.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

replylayer-0.16.0-py3-none-any.whl (58.3 kB view details)

Uploaded Python 3

File details

Details for the file replylayer-0.16.0.tar.gz.

File metadata

  • Download URL: replylayer-0.16.0.tar.gz
  • Upload date:
  • Size: 81.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for replylayer-0.16.0.tar.gz
Algorithm Hash digest
SHA256 6695d731450396c8eda3b5ced50a12283174ae3874390f0d2e0d9098cf11ecf1
MD5 c7701d88f7290a8368abb5e4b357080b
BLAKE2b-256 3ff0f548414d1d08fb80ea1a51a89365ca58d4cfbfd109e5393b099f0c7d5455

See more details on using hashes here.

Provenance

The following attestation bundles were made for replylayer-0.16.0.tar.gz:

Publisher: publish-pysdk.yml on replylayer/ReplyLayer

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file replylayer-0.16.0-py3-none-any.whl.

File metadata

  • Download URL: replylayer-0.16.0-py3-none-any.whl
  • Upload date:
  • Size: 58.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for replylayer-0.16.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0aa8496141ab4123ed8e04c3500d369536d208f77e58fcf1b52420da7e3c5b76
MD5 03cffd9613545dd7dc489ec885d65e6f
BLAKE2b-256 b3b385a97f314d0d71cda88fd401cb89c14db12fd0e7b8a387174448add5943c

See more details on using hashes here.

Provenance

The following attestation bundles were made for replylayer-0.16.0-py3-none-any.whl:

Publisher: publish-pysdk.yml on replylayer/ReplyLayer

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page