signalcore
SignalCore's server SDK for Python: your backend makes organizations for your customers, sets up
their agents, talks to them and hears what happens, through SignalCore's public API
(https://signalcore.ai/v1). Every operation of the API is a typed method; the SDK holds no logic
of its own.
pip install "signalcore>=5,<6" # uv add, poetry add alike
Python 3.10 or later. Version 5 is for SignalCore 2.0's API as it is now: everything the owner
does in the app (keys, contacts, an agent's access, dashboards, the inbox's approvals), and a
request of the wrong shape refused as every error is (ApiError with code == "invalid"). 4.x
answered those as code == "error"; 2.x was the API with squads, 0.x the API it replaced.
Full docs, in Spanish: signalcore.ai/docs.
Keys
A secret key (sk_…) is made by the account's owner in the app, in Tu plataforma → Claves
(the app shows Tu plataforma to an account of kind "Lo integro en mi producto"; any account can
switch in Ajustes → General → Tipo de cuenta). It is shown once: keep it in your server's
environment, never in a page. Each key has scopes:
| Scope | Lets the key |
|---|---|
read |
read everything (every GET) |
write |
change the organization: agents, their folder, files, tables, the vault, automations, triggers, channels, apps, contacts, who answers a conversation, the inbox, your tools |
talk |
start conversations, speak in them, call an agent, upload a conversation's files, write as the business on a channel, mint person tokens |
admin |
make and delete your customers' organizations, rename your own, deploy and update templates, make and remove webhooks, make, list and revoke keys |
A key without the scope gets 403 forbidden. Revoking a key ends every token it minted.
Once you have one with admin, your code makes the rest, never with more scopes than its own
(403 otherwise); a publishable key talks, so it takes read and talk:
made = client.keys.create(kind="secret", name="Reader", scopes=["read"])
made["key"] # sk_…: shown this once; listed after only by its `prefix`
client.keys.list() # newest first, the revoked ones too
client.keys.revoke(made["id"])
SignalCore(org="clinica-norte").keys.create(kind="secret", name="Theirs", scopes=["read"]) # a customer's own
from signalcore import SignalCore
client = SignalCore() # reads SIGNALCORE_API_KEY; or SignalCore("sk_…")
Keyword options: org (act on a customer's organization, below), base_url
(SIGNALCORE_BASE_URL, or https://signalcore.ai), timeout in seconds (90), http_client (an
httpx.Client of your own). AsyncSignalCore has every method of SignalCore, awaited, and its
http_client is an httpx.AsyncClient. Both close with close() or as a context manager.
Quickstart
agent = client.agents.create(
name="Recepción",
instructions="You answer the clinic's patients briefly, in Spanish.",
)
conversation = client.conversations.start(agent["id"])
reply = client.conversations.say(conversation["id"], text="¿Abrís el sábado?", wait=True)
print(reply["status"], reply["replies"][0]["text"])
Answers are plain dicts typed with TypedDicts (signalcore.types), so your editor knows every
field.
Agents
agents = client.agents.list() # the builder first (is_builder), then by name
one = client.agents.get("agt_…")
client.agents.update(
one["id"],
instructions="Answer in Spanish, in two sentences at most.",
model="claude-opus-5-5", # client.catalog.models() lists the ids
timezone="Europe/Madrid",
)
client.agents.delete(one["id"]) # the builder can't be deleted
agents.create takes name and, optionally, role, description, instructions, model and
icon; a name the organization has already is 409 conflict. agents.update changes only what
it is passed, and also takes voice (how it talks on calls), packages (what its own code
installs), actions and needs_approval, and what the agent may set up itself in a
conversation: connects, the apps it connects with connect_app (which kinds or toolkits,
whose account, what it gets of one, who gives the yes for its writes), and switches, the
automations it turns on or off with switch_automation:
client.agents.update(
one["id"],
actions=[*one["actions"], "connect_app", "switch_automation"],
connects={
"kinds": ["crm"],
"whose": "person",
"gets": "all",
"writes": "person",
"return_url": "https://your.app/chat", # where the person lands once they authorized it
},
switches=[{"agent": "Coach", "automation": "weekly_report"}], # no agent: its own
)
Knowledge
Files every agent of the organization reads, or one agent's own. searchable=True indexes it
(PDF, Word, Excel, PowerPoint, text) so the agent searches it when asked; up to 50 MB a file.
from pathlib import Path
client.files.put(
"tarifas.pdf",
Path("tarifas.pdf").read_bytes(),
searchable=True,
content_type="application/pdf",
)
client.agents.files_put(
agent["id"],
"guion.md",
"# Cómo saludar\n…".encode(),
searchable=True,
content_type="text/markdown",
)
hits = client.files.search(query="precio de una limpieza") # [{path, location, text, …}]
data = client.files.get("tarifas.pdf") # bytes
client.files.delete("tarifas.pdf")
# Something the agent keeps in mind, as it would learn it itself
client.agents.memories_create(agent["id"], text="Los sábados abrimos de 10:00 a 14:00.")
Tables (client.data), keys the agent uses on the web (client.vault,
client.agents.vault_put) and the apps it uses (client.connections) are in the same client.
The agent's own actions
An agent is a folder: agent.yaml, instructions.md and its code. A file in actions/ with one
@sc.action function is a tool the model calls while it talks:
PRECIO = '''import sc
PRECIOS = {"limpieza": 45.0, "blanqueamiento": 180.0}
@sc.action
def precio(servicio: str) -> float:
"""El precio de un servicio en euros. Llámala cuando pregunten cuánto cuesta."""
return PRECIOS[servicio.lower()]
'''
client.agents.folder_put(agent["id"], "actions/precio.py", content=PRECIO)
actions = client.agents.actions_list(agent["id"]) # precio, with source "own"
files = client.agents.folder_list(agent["id"]) # every file of the folder
A file that isn't valid code is refused (422) with the reason. automations/<name>.py is what
the agent does on its own; see client.automations (test, switch) and client.runs.
What it may use
What the builder gives and takes, one thing at a time (write): a table and its verbs, an app's
tools (connections.actions lists them), a key of the organization's vault. Each answers the
agent, its access and actions as they are now.
client.agents.tables_give(agent["id"], "leads", can=["find", "add"])
client.agents.tables_give(agent["id"], "bookings", can=["find"], contact="client") # theirs only
client.agents.apps_give(agent["id"], "gmail", tools=["Gmail.ListEmails"])
client.agents.keys_give(agent["id"], "stripe")
client.agents.tables_take(agent["id"], "leads") # apps_take, keys_take alike
A table changes after it was made: fields added or changed by name, dropped, a new title. A field
dropped or changed in type loses its values. The contacts table takes your own fields the same
way, and contacts.create(fields=…) fills them.
client.data.update("leads", fields=[{"name": "source", "type": "text"}], drop=["vip"], title="Leads 2026")
Dashboards are written as the builder writes them: blocks over the tables its agents use, each
query checked first (422 with why, and nothing kept).
board = client.dashboards.create(
name="sales",
title="Sales",
blocks=[{"type": "stat", "label": "Leads", "query": {"table": "leads", "measure": "count"}}],
)
client.dashboards.update(board["id"], title="Sales this month") # only what is sent
client.dashboards.render(board["id"]) # the figures now
Automations
An automation is code in the agent's folder (automations/<name>.py, see the guide
"Automatizaciones en código"): written with folder_put, tried with a dry run, and switched on,
since a new one starts paused.
client.agents.folder_put(agent["id"], "automations/reminder.py", content=REMINDER)
client.automations.test(agent["id"], "reminder") # a dry run, answered when it ends
client.automations.switch(agent["id"], "reminder", on=True)
Your backend runs one now, for one of your users when it works for one, kept as a run by api:
one on a schedule, one only started by hand (@sc.manual(inputs={...})) or a webhook, given every
input it declares. What its code returns is the run's result (also in run.finished); one
written @sc.after("<automation>") starts when that one ends well, given its result. An app's
event or a channel's message start the rest: try those with test.
run = client.automations.run(agent["id"], "weekly_summary", contact={"account": "user_42"},
inputs={"week": 42}, wait=True)
run["status"], run["result"] # "done", {"deals": 3, ...}
Conversations
conversation = client.conversations.start(agent["id"])
reply = client.conversations.say(conversation["id"], text="Hola", wait=True)
if reply["status"] == "done":
print([message["text"] for message in reply["replies"]])
elif reply["status"] == "waiting":
print(reply["waiting"]) # [{id, action, kind: ask | approve | tool, decides}]
elif reply["status"] == "failed":
print(reply["error"]) # {code, detail}
else: # "running": still going after a minute; read it later
...
whole = client.conversations.get(conversation["id"]) # with its messages
latest = client.conversations.list(agent["id"]) # newest first
With wait=True the call answers when the agent's turn ends, up to a minute. Without it, it
answers at once with running; the reply comes on the live socket or a webhook. While a turn runs,
another say is 422 busy; while the turn waits on a question, 422 pending.
When an action waits for the business's yes (decides: "business", also told by the
approval.requested webhook), your backend gives it:
client.conversations.decide(conversation["id"], request=reply["waiting"][0]["id"], approve=True)
# Several at once, all kept or none; a yes may change the call's input first (its `schema`)
client.conversations.decide_many(conversation["id"], [
{"request": "toolu_1", "approve": True, "input": {"amount": 1200}},
{"request": "toolu_2", "approve": False},
])
Threads, unread, and the agent first
Your backend opens a thread in a user's own chat with its key, no token: titled, with its first
words the user's (the agent answers) or the agent's. Their page lists their threads with unread
and waiting_on_person, and marks what they read (conversations.read).
thread = client.conversations.start(agent["id"], contact={"account": "user_42"},
title="Demo with Acme", say={"text": "Help me prep", "from": "contact"})
client.conversations.update(thread["id"], title="Acme, Thursday")
client.contacts.conversations(contact_id) # each with unread, waiting_on_person
The agent writes first (say.from = "agent", or sc.say from an automation) with no limits of
SignalCore's: only each channel's rules hold (WhatsApp's and Instagram's 24 h window and
templates, and never to who opted out). Such messages carry proactive: true.
Files in a conversation
Upload first, then name the file in say (up to 10 a message):
invoice = client.conversations.files_create(
conversation["id"],
Path("factura.pdf").read_bytes(),
file_name="factura.pdf",
content_type="application/pdf",
)
client.conversations.say(
conversation["id"], text="¿Me explicas esta factura?", files=[invoice["id"]], wait=True
)
Live
Every frame of a conversation as it happens:
with client.conversations.live(conversation["id"]) as live:
live.say("Hola")
for frame in live:
if frame["type"] == "text":
print(frame["delta"], end="", flush=True)
if frame["type"] == "done":
break
With AsyncSignalCore, the same with async with, await live.say(…) and async for.
What comes down: working (a turn began), message (a message stored, in message), text (a
piece of the reply, in delta), step (an action: running, then done, failed or
waiting), ui (a screen the agent drew), request (an action that asks first), tool (a
client tool for the person's page), handoff (it went to a person), holder (who answers
changed), done (the turn is over), stopped, and error (code, detail). What you send:
live.say(text, file_ids), live.stop(), or any frame with live.send({"type": …}). Closing
the socket doesn't stop the reply: it is stored.
Your customers' organizations
An agency or a product makes one organization per customer (admin scope). Each comes with its
own builder and shares nothing with the others. Name the agents it starts with: your agents that
are templates (below).
customer = client.orgs.create(name="Clínica Norte", templates=[agent["id"]])
# {id, name, slug: "clinica-norte", kind: "business", agents: [ … ]}
# Act on it: the same key, with `org` as its id or its slug (SignalCore-Org)
theirs = SignalCore(org=customer["slug"])
theirs.agents.list() # their own agents
theirs.files.put("carta.pdf", data, searchable=True, content_type="application/pdf")
every = client.orgs.list() # oldest first
same = client.orgs.get(customer["id"])
client.orgs.delete(customer["id"]) # 409 while it has channels or connected apps left
What each customer used this month, for your bill (from_ and to, a datetime or ISO 8601,
for another stretch):
for used in client.usage.managed():
print(used["org_id"], used["cost"]) # USD
Each of them at a glance, as your home in the app shows them this month (read):
home = client.customers.overview()
for customer in home["customers"]:
# conversations, cost (USD), and what needs someone: lost channels, failed automations,
# approvals the business has to give
print(customer["org"]["name"], customer["conversations"], customer["approvals"])
Templates
One of your agents, given to your customers: each customer's organization gets its own copy, and your changes reach every copy except what that customer changed.
client.templates.deploy(agent["id"]) # version 1, as the agent is now
# Your deploy owns these fields: from now on only it writes them, in every copy
client.templates.deploy(
agent["id"],
spec={"instructions": "…", "files": {"actions/precio.py": PRECIO}},
)
client.agents.update(agent["id"], model="claude-opus-5-5")
status = client.templates.update_all(agent["id"]) # a new version, brought to every copy
# status["agents"]: [{org_id, org_name, agent_id, version, overrides}]
theirs.templates.make(agent["id"]) # a copy in an organization that exists (SignalCore-Org)
client.templates.get(agent["id"]) # the template and every copy
client.templates.delete(agent["id"]) # no longer a template; the copies stay
spec takes any of the agent's fields: name, role, description, icon, instructions,
model, timezone, voice, packages, actions, asks, access, tools, files.
People in your product
Your backend mints a token for each user (an hour, the agents it names; talk scope), and your
page talks with it through @signalcore/ui:
token = client.tokens.create(
agents=[agent["id"]], external_id=user.id, name=user.name, email=user.email
)
return {"token": token["token"], "expires_at": token["expires_at"]}
For anonymous visitors of a site, connect the agent's web channel; its publishable key (pk_…)
goes in the page, and works only from the origins named:
connected = client.channels.web_connect(agent_id=agent["id"], origins=["https://clinicanorte.com"])
key = connected["channel"]["web"]["key"]
# <script src="https://signalcore.ai/v1/widget.js" data-key="pk_…" async></script>
Surfaces: views and your own blocks
What the agent shows can go where it fits in your product (v3/UI.md, Surfaces). A view is what
the agent keeps on show for each person: a dashboard's spec over the tables the agent was given,
rendered for one contact with their own rows. Your page draws it with @signalcore/ui's
<View>; your backend can render it too.
client.views.create(
agent["id"],
name="proxima_reunion",
title="Tu próxima reunión",
blocks=[
{
"type": "list",
"title": "Tu próxima reunión",
"item": "titulo",
"meta": "inicio",
"query": {
"table": "reuniones",
"where": [{"field": "inicio", "op": "next_days", "value": 14}],
"order": "inicio",
"descending": False,
"limit": 1,
},
}
],
)
rendered = client.views.render("proxima_reunion", contact=contact["id"])
# {name, title, contact_id, blocks: [...the kit's, filled], text, rendered_at}
Your own blocks: declare one once (an admin key) with the JSON Schema of its props, register
its component in your page by the same name, and the agent may show it giving only props, which
the API checks against the schema.
client.blocks.put(
"demoprep.deal-card",
description="A deal as the rep's pipeline shows it.",
schema={
"type": "object",
"properties": {"title": {"type": "string"}, "amount": {"type": "number"}},
"required": ["title", "amount"],
},
)
Contacts
The people your agents serve: who writes or calls on a channel, or a user of your product with a token. A conversation names its contact:
conversation = client.conversations.get("cnv_…")
if conversation.get("contact"):
contact_id = conversation["contact"]["id"]
contact = client.contacts.get(contact_id) # {id, name, muted, opted_out, created_at}
memories = client.contacts.memories(contact_id) # what the agents keep about them
tasks = client.contacts.tasks(contact_id) # what they asked for later
their_conversations = client.contacts.conversations(contact_id)
client.contacts.update(contact_id, muted=True) # the agent stops answering them
Your backend adds the people it knows, by the id your product knows them by (account), a phone,
an email or an Instagram id. The first identity given finds them when they are known: the same
contact comes back (200, not 201), the other identities added. fields are the contacts
table's fields of yours.
marta = client.contacts.create(account="user_42", name="Marta", phone="+34600111222", fields={"plan": "pro"})
client.contacts.list(phone="+34 600 111 222") # or email, account, instagram, q (words)
client.contacts.list(field=["plan:pro"]) # by a field of yours
for contact in client.contacts.iter(): # every one, page after page (100 at most each)
print(contact["id"], contact["account"])
Give a contact one of the agent's automations for contacts (for_="contact" in its code), as the
agent's schedule_task does: what it takes, and a cron line or a time once (at).
client.tasks.create(agent["id"], contact=marta["id"], automation="digest", inputs={"zone": "north"}, cron="0 8 * * 1")
One person, many identities: add or take away how a contact is known (unique in the organization:
another contact's is 409), and merge two contacts that are one person into the one kept (their
conversations, memories, tasks, files, keys and apps move; two files, keys or apps of the same name
are refused first).
client.contacts.update(marta["id"], add=[{"kind": "phone", "value": "+34600111222"}])
client.contacts.merge(marta["id"], from_="ctc_…")
The inbox
What needs a person: notes, handoffs, failures, and the conversations that wait. Each item names
its contact; one that waits lists what it waits on (requests), so your backend gives the
business's yes (talk):
for item in client.inbox.list(kind="waiting"): # agent=, contact= too
for request in item["requests"]:
if request["decides"] == "business":
client.conversations.decide(item["conversation_id"], request=request["id"], approve=True)
What is one person's is kept on their contact, and an agent uses it only where it works for
them, never with anyone else: their files (what they sent on any channel lands in received/),
their keys of the vault (names only come back) and what the agents know about them.
client.contacts.files_put(contact_id, "poliza.pdf", data, searchable=True, content_type="application/pdf")
client.contacts.files(contact_id) # [{path, media_type, size, searchable, …}]
client.contacts.files_get(contact_id, "received/foto.jpg") # bytes
client.contacts.vault_put(contact_id, "portal", value="…", hosts=["portal.example.com"])
client.contacts.memories_create(contact_id, text="Prefiere las tardes.", agent_id=agent["id"])
Your tools
Functions of your own the agent may call. A server tool is a URL of yours that SignalCore
POSTs to, signed; a client tool runs in the person's page (@signalcore/ui answers it).
declared = client.tools.put(
agent["id"],
"order_status",
description="The status of an order. Call it when the person asks where their order is.",
input_schema={
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"],
},
mode="server",
url="https://api.tuapp.com/signalcore/order-status",
)
# declared["secret"]: the organization's tool secret, only with its first server tool
# (a new one: client.tools.rotate_secret())
Each call is {tool, input, conversation_id, org_id, person}, signed with the tool secret as a
webhook is, so the same verify checks it. Answer JSON within 30 seconds:
from signalcore import SignatureError, verify
@app.post("/signalcore/order-status")
async def order_status(request: Request):
try:
call = verify(await request.body(), request.headers, TOOL_SECRET)
except SignatureError:
raise HTTPException(400)
return await orders.status(call["input"]["order_id"])
Your API has an OpenAPI document? Import it and pick operations instead:
client.tools.import_openapi(agent["id"], url=…), then
client.tools.pick_openapi(agent["id"], picked=["listOrders"]).
Webhooks
hook = client.webhooks.create(
url="https://api.tuapp.com/signalcore",
events=["message.created", "approval.requested", "run.failed"],
)
# hook["secret"] (whsec_…): shown this once
Events: message.created, conversation.holder_changed, inbox.item_created, run.finished,
run.failed, call.ended, channel.status_changed, approval.requested. Each arrives as
{id, type, created_at, org_id, data}; check it before trusting it, with the body exactly as it
came:
from signalcore import SignatureError, verify
@app.post("/signalcore")
async def hook(request: Request):
try:
event = verify(await request.body(), request.headers, WEBHOOK_SECRET)
except SignatureError:
raise HTTPException(400)
if event["type"] == "message.created":
...
return Response(status_code=204)
Answer a 2xx within 10 seconds. Otherwise the delivery is tried again (after 15 s, 30, 60 and
120) and is dead after the fifth try; id is the same each try. To send one again:
for delivery in client.webhooks.deliveries(hook["id"]):
if delivery["status"] == "dead":
client.webhooks.retry(hook["id"], delivery["id"])
client.webhooks.verify is the same function as verify.
Usage
from datetime import datetime, timezone
usage = client.usage.get() # this month so far
# {cost, used: [{kind, unit, quantity, count, cost, unpriced}], messages: [{channel, received, sent}]}
october = client.usage.get(
from_=datetime(2026, 10, 1, tzinfo=timezone.utc), to=datetime(2026, 11, 1, tzinfo=timezone.utc)
)
Errors, retries and pages
Whatever the API refuses or fails raises one ApiError: status (HTTP), code to decide by and
detail, a sentence you can show.
from signalcore import ApiError
try:
client.agents.get("agt_…")
except ApiError as error:
if error.code == "not_found":
...
status |
code |
Means |
|---|---|---|
| 400 | bad_request |
the request can't be done that way |
| 401 | unauthorized |
no key, or not valid |
| 403 | forbidden |
the key lacks the scope |
| 404 | not_found |
it doesn't exist, or isn't yours |
| 409 | conflict |
it clashes with something that exists |
| 410 | gone |
it was there and isn't any more: expired or used |
| 422 | invalid, busy, pending, files, … |
the request can't be done; a body of the wrong shape is invalid, its detail naming the field first (scopes.0: …) |
| 429 | rate_limited |
more than 600 calls a minute for the key |
The SDK doesn't retry by itself. On 429, wait and try again:
import time
def with_retry(call, tries=5):
for attempt in range(1, tries + 1):
try:
return call()
except ApiError as error:
if error.status != 429 or attempt == tries:
raise
time.sleep(2**attempt)
agents = with_retry(lambda: client.agents.list())
Every write takes idempotency_key=: sent again with the same key within 24 hours, it answers the
first answer and isn't done twice.
client.conversations.say(conversation["id"], text="Hola", idempotency_key=f"msg-{message_id}")
client.orgs.create(name="Clínica Norte", idempotency_key=f"customer-{customer_id}")
Three lists come in pages, read whole with iter; every other list answers at once:
for contact in client.contacts.iter(field=["plan:pro"]):
print(contact["id"])
for run in client.runs.iter(agent=agent["id"], status=["failed"]):
print(run["id"])
for item in client.activity.iter(kind="conversation"):
print(item)
Every operation
Each operation of the API reference is a method named
from its id, <resource>_<verb>: agents_get is client.agents.get(agent_id),
agents_memories_create is client.agents.memories_create(…), templates_update_all is
client.templates.update_all(…). signalcore.OPERATIONS maps every id to its method. What is
deprecated says so.
- Path parameters are positional; the body's fields and the query are keyword arguments.
- A field left out isn't sent;
Nonesendsnull(signalcore.OMITleaves one out explicitly). - Files go up as bytes with
content_type=and come down as bytes. - A tool's new name in
client.tools.editisnew_name=.
Developing it
Half of the package is generated from the API's contract (v3/api/openapi.json):
_resources.py and _types.py, by python v3/clients/generate.py. The rest is by hand:
_base.py (requests, errors, pages), _extras.py (the live socket, webhooks). When the contract
changes, generate again; CI fails while they differ.
uv sync
uv run pytest # against the local API (v3-api) at http://localhost:8300, or SIGNALCORE_TEST_API
uv run mypy && uv run ruff check
The tests that need an API sign up an owner of their own there and skip themselves when none answers.
Metadata
Release files for signalcore 5.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| signalcore-5.3.0.tar.gz | 125.2 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| signalcore-5.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 187.8 kB
Release files / signalcore-5.3.0.tar.gz
| Download URL | signalcore-5.3.0.tar.gz |
|---|---|
| Size | 125.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ec941e38722cde0b287794f5663d9a519e0f949202901cc202c64e4b1877c0b7
|
|
BLAKE2b-256 checksum How to use checksums |
5070ab3c9d8f715c2b209396aa4a6b605aef3a85af7269c1100b310b9a7f3d66
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / signalcore-5.3.0-py3-none-any.whl
| Download URL | signalcore-5.3.0-py3-none-any.whl |
|---|---|
| Size | 62.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
74b8b2729e5f93e60d606ca649bb73a9a7d581e8cd92db3f045478cfc8a3fd9e
|
|
BLAKE2b-256 checksum How to use checksums |
ed4e1ae678705014d26acda4bc8332c39382fa7654866bbdb69dbadc2a06fd76
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logSecurity for signalcore 5.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for signalcore-5.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.