Publish and consume
This page covers publishing and consuming messages across Pub/Sub, including pull and push delivery modes, acknowledgement mechanics, and native wire protocol integration.
Environment configuration
export CAI_PUBSUB_API="https://api.codyhill.dev"
export CAI_TOKEN="<your-api-key-or-session-token>"
export CAI_PROJECT="<your-project-id>"
Publishing messages
Publishers send payloads to topics using string attributes, ordering keys, and binary or text data.
Publish payload structure
- Payload Body: Set either
text(plain text string) ordata(base64 encoded bytes). - Attributes: Key-value pairs for metadata or consumer filtering.
- Ordering Key: String key used by
key-sharedsubscriptions to guarantee in-order delivery per key. - Batch Limits: Up to 100 messages or 4 MiB per publish request.
- platformctl
- curl
- Console UI
platformctl pubsub topics publish orders --message "order-created" --attribute region=eu
Output:
1234:0
curl -sX POST "$CAI_PUBSUB_API/v1/projects/$CAI_PROJECT/topics/orders:publish" \
-H "Authorization: Bearer $CAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"messages":[
{"text":"order-created","attributes":{"region":"eu"},"key":"customer-42"},
{"data":"aGVsbG8gYWdhaW4=","attributes":{"datacontenttype":"application/json"}}
]}'
Output:
{
"message_ids": ["1234:0", "1234:1"]
}
- Navigate to Messaging → Pub/Sub and select your topic.
- Click Publish Message.
- Fill in message payload text and attributes, then click Publish.
Consuming with Pull delivery
Pull subscriptions allow consumers to fetch messages on demand and acknowledge them upon successful processing.
Pulling messages
- platformctl
- curl
- Console UI
platformctl pubsub subscriptions pull workers --topic orders --max 10
curl -sX POST "$CAI_PUBSUB_API/v1/projects/$CAI_PROJECT/topics/orders/subscriptions/workers:pull" \
-H "Authorization: Bearer $CAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"max_messages":10,"timeout_ms":2000}'
Output:
{
"messages": [
{
"ack_id": "CAEQAB...",
"id": "1234:0",
"data": "b3JkZXItY3JlYXRlZA==",
"attributes": {"region": "eu"},
"key": "customer-42",
"publish_time": "2026-08-12T10:00:00Z",
"delivery_attempt": 1
}
]
}
- Open the subscription detail page.
- Click Pull Messages to display waiting messages in the table.
Acknowledging messages
Acknowledge messages before the ack_deadline_seconds expires to prevent automatic redelivery.
- platformctl
- curl
- Console UI
platformctl pubsub subscriptions ack workers --topic orders CAEQAB...
Or pull and acknowledge automatically:
platformctl pubsub subscriptions pull workers --topic orders --max 10 --ack
curl -sX POST "$CAI_PUBSUB_API/v1/projects/$CAI_PROJECT/topics/orders/subscriptions/workers:acknowledge" \
-H "Authorization: Bearer $CAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ack_ids":["CAEQAB..."]}'
Click Acknowledge next to pulled messages on the subscription page.
Consuming with Push delivery
Push subscriptions deliver messages automatically via HTTP POST requests to an endpoint within your project.
Creating a push subscription
- platformctl
- curl
- Console UI
# WORKER_URL is the "url" field that 'platformctl status order-worker' prints
platformctl pubsub subscriptions create pusher --topic orders \
--push-url "$WORKER_URL:8080/events" \
--push-content-mode cloudevents-binary \
--dead-letter-topic orders-dead --dead-letter-after-attempts 5
Do not compose that address by hand. Copy the url field that platformctl status order-worker prints and append the port and path your handler listens on.
# WORKER_URL is the "url" field that 'platformctl status order-worker' prints
curl -sX POST "$CAI_PUBSUB_API/v1/projects/$CAI_PROJECT/topics/orders/subscriptions" \
-H "Authorization: Bearer $CAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"pusher","type":"shared",
"deliver":{"mode":"push","push":{"url":"'"$WORKER_URL"':8080/events",
"content_mode":"cloudevents-binary"}},
"dead_letter":{"topic":"orders-dead","after_attempts":5}}'
- Create a new subscription on your target topic.
- Select Push mode and enter your target workload URL.
- Choose the CloudEvents binding (
binaryorstructured). - Select a Dead-Letter topic and click Save.
The check is an allowlist, not a blocklist of bad addresses. The host has to be the private address of a workload in this subscription's own project — the address the platform already hands you, printed as the url field of platformctl status <workload> (the shortened leading form of that same address is accepted too). Anything else is refused with 400:
deliver.push.url is outside this project. A push target must be a workload in this
project, and the address to use is the one the platform already gives you: the "url"
field on that workload, which 'platformctl status <workload>' prints.
An invented hostname such as my-service.project.internal, a bare hostname, an IP literal, localhost, an external domain, and a workload in someone else's project all fail that way — as does the delivery machinery's own address, so a push target cannot be turned back on the platform. The scheme must be http — https to a private address has no per-project trust material to verify against, so it is refused rather than delivered unverified — and userinfo in the URL is refused, because it would be written into the subscription object and its logs.
CloudEvent delivery bindings
cloudevents-structured(the default): Delivers a single JSON payload containing the CloudEvent metadata envelope and the message data, underContent-Type: application/cloudevents+json. Noce-*headers are sent.cloudevents-binary: Delivers the raw message body with CloudEvent metadata passed via HTTP headers (ce-id,ce-type,ce-source).
Omit --push-content-mode and you get structured, so a handler written to read ce-* headers sees none of them. Ask for binary explicitly if that is what your code expects.
Native wire protocol integration
Crusoe AI Platform Pub/Sub speaks the Pulsar wire protocol, so a stock Pulsar client library lets your own code produce and consume over the binary protocol instead of the REST API.
service_url is a private address: it resolves only from a workload running inside this project, and it is the supported path for direct clients. Reaching the broker from outside the platform is a separate thing you have to ask for — nothing here answers on the internet until this project's Pub/Sub endpoint is published, and until then the credentials response carries no external address at all, with a note saying so.
Project administrators can request the project's client credentials:
- platformctl
- curl
- Console UI
platformctl pubsub credentials
curl -s "$CAI_PUBSUB_API/v1/projects/$CAI_PROJECT/pubsub/credentials" \
-H "Authorization: Bearer $CAI_TOKEN"
Output:
{
"service_url": "pulsar://<private-host>:6650",
"token": "eyJhbGciOi...",
"topic_prefix": "persistent://p-ab12cd/main/",
"expires_at": "2026-09-12T10:00:00Z",
"last_used_at": "2026-08-27T14:03:00Z",
"note": "this credential can produce and consume only inside this project. It EXPIRES at expires_at (D0081); renew by requesting credentials again before then - the old token stays valid until its own expiry, so renewal has no outage window. There is no address for reaching Pub/Sub from outside the platform: nothing is reachable from the internet until you publish it. Publish this project's Pub/Sub endpoint to get one."
}
Once the project's Pub/Sub endpoint is published, the response gains external_endpoint (pulsar+ssl://<hostname>:443, repeated under the older key external_service_url) and reachable_scope: "internet" — that address is reachable from anywhere, with the token as the whole boundary, so narrow it with an address allow list on the endpoint or unpublish it. Read the note before you build on it: today only connect and topic lookup are proven over the external path. A lookup answers with the owning broker's internal address, so full produce/consume from outside is not available yet. Use service_url from inside the project for real traffic.
The token expires — expires_at says when the broker starts refusing it. Renew by calling this endpoint again; the old token keeps working until its own expiry, so a rotation does not cut a running client off mid-flight. last_used_at and usage_sources report where the credential has actually been used, newest first; a new external source on an old credential is the anomaly worth looking at.
Go to Messaging → Pub/Sub and scroll to Connect from outside the platform, then click Show connection details. The details include the project's Pub/Sub token, so only a project admin can reveal them — a member sees the section with an explanation instead of the button.
Publishing from a deployed workload
Everything above authenticates as you, against the public API host. A function or agent running on the platform does neither, and copying these examples into a handler produces a failure that points in the wrong direction.
Use the injected private address, not the public hostname. Every workload gets CAI_PUBSUB_URL and CAI_PROJECT_ID in its environment — read the variables, do not hard-code what they hold. The public API host, https://api.codyhill.dev, does not route from inside a project; a handler that reaches for it hangs until its own timeout.
Use a service-account key, not the workload's own key. CAI_PROJECT_KEY is also injected, and it is refused on every project route by design — its one power is minting a token to read this project's secrets. The refusal is 404 not found, not 403, because the platform does not disclose whether a project exists to a caller with no grant on it. That means a wrong credential looks exactly like a misspelled topic. Check the credential first.
So: create a service account, give it the member role (enough to publish; creating topics and subscriptions needs admin), mint a key, store it as a project secret, bind the secret to the function, and Apply the binding — a bound secret only reaches the workload on the next revision.
import json, os, urllib.request
PUBSUB = os.environ['CAI_PUBSUB_URL'].rstrip('/')
PROJECT = os.environ['CAI_PROJECT_ID']
KEY = os.environ['PIPELINE_KEY'] # the bound service-account key
def publish(topic, payload):
req = urllib.request.Request(
'%s/v1/projects/%s/topics/%s:publish' % (PUBSUB, PROJECT, topic),
data=json.dumps({'messages': [{'text': json.dumps(payload)}]}).encode('utf-8'),
headers={'Content-Type': 'application/json', 'Authorization': 'Bearer ' + KEY},
method='POST')
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode('utf-8') or '{}')
A handler that reads os.environ['PIPELINE_KEY'] at import time fails its first revision every time: bind the secret, then Apply, and the revision that comes up after the Apply is the one that has it.
Delivery guarantees summary
| Guarantee | Details |
|---|---|
| Delivery Count | At-least-once delivery guaranteed across active subscriptions. |
| Ordering | Configurable per subscription (shared un-ordered, key-shared per-key ordered). |
| Redelivery | Triggered if an unacknowledged pull message exceeds ack_deadline_seconds. |
| Push Retries | Exponential backoff retries up to max_deliver attempts before dead-letter routing. |