Send your first push
#CC’ed gives your Apple devices a private URL. Connect an AI agent, script, or automation to that URL and its message arrives as a native push notification. If it can make an HTTP request, it can reach you through CC’ed.
Before you start, install CC’ed on at least one Apple device and allow notifications. CC’ed includes 14 complimentary days with full notification content; after that, CC’ed Plus keeps sender content visible. Need the app? Try the beta on TestFlight.
Start here
#1. Enable notifications
#Open CC’ed and allow notifications when asked. The app connects this device to its private route automatically.
2. Send the built-in test
#On the Home screen, tap Send test below the ready-made command. CC’ed sends a test without exposing your private URL to shell history. Choose Time Sensitive from the example menu to test the Focus-aware delivery path.
3. Look for the notification
#The notification should arrive within a few seconds. That is the whole setup. You can now use the same private URL from AI agents, scripts, automations, monitoring tools, or anything else that can send an HTTP request.
Let an AI coding agent configure itself
#Give the following instructions to the agent you want to connect.
Do not include the private webhook.
Configure CC’ed using your native lifecycle hooks for turns ending, failures, and times when you are waiting for my permission or input. Install it for my user account, not in the current repository. For Codex, use
notify(agent-turn-complete) when a turn ends andPermissionRequestwhen approval is actually required. For Claude Code, useStop,StopFailure,Notificationforpermission_prompt|elicitation_dialog, and asynchronousPreToolUseforAskUserQuestion|ExitPlanMode. Addagent_needs_inputonly when the installed Claude version supports it and you can prevent duplicate alerts. For GitHub Copilot CLI, useagentStop,notificationforpermission_prompt|elicitation_dialog, anderrorOccurredonly whenrecoverableis false. For Pi, inspect the latestagent_endmessages immediately, retain only a sanitized bounded final excerpt and failure classification, then send once onagent_settled, after automatic retries, compaction, and queued follow-ups have stopped. For OpenCode, usesession.statuswhen its status isidle,session.error,permission.asked, andquestion.askedwhen the installed version exposes it. Otherwise usetool.execute.beforewhen the tool isquestion, but never both question paths. Retain only a sanitized, bounded assistant excerpt until the session becomes idle: usemessage.updatedto identify assistant message IDs, then accept only correlatedmessage.part.updatedcontent whosepart.typeistext, never reasoning, tool, synthetic, or ignored content. First create a local terminal helper that reads the webhook without echo and saves its secret in the operating system’s secret store. I will run that helper myself outside the agent and conversation. Load the secret only when a hook runs. Send withPOST https://api.cced.app/v1/sendandAuthorization: Bearer <secret>. Keep the private URL and bearer value out of source files, hook configuration, process arguments, shell history, and logs. Build a short useful notification from the event’s user-facing fields. Include the agent name and whether the turn ended, failed, or needs attention. Only call a Claude turn finished whenbackground_tasksandsession_cronsare empty. When the event provides one, include one relevant detail: a concise excerpt of the final assistant message, the notification title and message, the clarifying question, or the tool name and human-readable permission description. Limit that detail to 240 characters. Group every completion, failure, permission, and clarification notification for the same agent task by sending the same CC’edthread_id. When the hook supplies identifiers, choose one canonical identity for that agent: prefer its top-level task, thread, or conversation identifier, and use a session identifier only when it is the sole stable task identity. Reuse that exact choice and derivation in every lifecycle handler. Derive an opaque deterministic value locally—for example, an HMAC-SHA-256 digest keyed by the webhook secret, domain-separated by agent name, and encoded as hex or base64url—and keep the result within 256 UTF-8 bytes. Never send or log the raw identifier. If the event has no stable task identity, omitthread_idrather than grouping unrelated work. Never substitute the working directory or transcript path. CC’ed maps this field to Apple’s notification thread identifier. Never send or log the raw hook payload, raw command or tool arguments, working directory, transcript path, or session ID, and never open the transcript just to create a notification. Inspect the existing hook configuration first and preserve every unrelated hook. Reuse or update an equivalent CC’ed hook instead of adding a duplicate. These are notification-only side effects: they must never approve, deny, continue, block, or inject context into the agent. Wrap the sender so secret lookup, parsing, or network failure still emits no stdout or stderr and exits zero. Use a short timeout for synchronous completion and failure senders. In Claude Code, use asynchronous delivery only for the attention-sideNotificationandPreToolUsehooks.
Use the agent’s supported lifecycle events rather than polling its process or watching terminal output. Configure every event listed for that agent:
| Agent | Turn ended or error needs attention | Waiting for permission or input | User-level location |
|---|---|---|---|
| Codex | notify (agent-turn-complete) |
PermissionRequest; a normal question arrives with the completed turn |
~/.codex/config.toml and ~/.codex/hooks.json |
| Claude Code | Stop; StopFailure for terminal API errors |
Notification matching permission_prompt|elicitation_dialog; asynchronous PreToolUse matching AskUserQuestion|ExitPlanMode; optional agent_needs_input for supported background sessions |
~/.claude/settings.json |
| GitHub Copilot CLI | agentStop; errorOccurred with recoverable false means a nonrecoverable execution error needs attention |
notification matching permission_prompt|elicitation_dialog |
~/.copilot/hooks/*.json |
| Pi | agent_settled after immediately reducing the latest agent_end messages to one sanitized bounded excerpt and failure classification; classify a terminal assistant stopReason of error and its errorMessage as failure |
No native permission or structured-question lifecycle event; a question in the final assistant message arrives when the agent settles | ~/.pi/agent/extensions/*.ts |
| OpenCode | session.status with status idle, using a bounded text excerpt correlated from assistant message.updated metadata and message.part.updated; session.error for session errors; track top-level sessions from session.created, session.updated, and session.deleted |
permission.asked; current-source question.asked, or tool.execute.before for the built-in question tool when that event is unavailable |
~/.config/opencode/plugins/*.js or *.ts |
Keep internal lifecycle noise out of CC’ed. For Claude Code, idle_prompt
duplicates foreground Stop, while agent_completed and SubagentStop report
background or internal completion. agent_needs_input requires Claude Code
2.1.198 or later, works only while background agent view is open, and can
overlap AskUserQuestion; enable it only with exact-one verification. For
Copilot CLI, do not register agent_idle, agent_completed, or subagentStop
unless the user explicitly wants internal agent notifications. Its
notification hook is CLI-only, not available in Copilot cloud agent.
Codex does not currently document a dedicated terminal-failure or structured
clarification event. A question in Codex’s final response is delivered by
agent-turn-complete, but an active structured-input request may not be. Do not
claim coverage the installed Codex client cannot prove. Codex notify passes
one JSON argument, while PermissionRequest sends JSON on standard input; the
sender must handle both forms. Copilot’s agentStop
event does not provide the final assistant message, so its completion
notification must be generic; do not read the transcript to manufacture an
excerpt. Before enabling Copilot failure alerts, verify whether the installed
CLI also emits agentStop for that failed turn and keep only one notification.
Pi’s agent_end is too early for a completion notification because Pi may
automatically retry, compact and retry, or process a queued follow-up. Inspect
its messages immediately, retain only the sanitized bounded final excerpt and
failure classification needed for the notification, and discard the full
message array before the handler returns. Then send from agent_settled; do not
send from both events. Pi does not expose a dedicated
failure or waiting-for-input event, so do not claim live attention coverage for
permissions or structured questions. OpenCode plugins receive lifecycle events
through their event handler. Treat session.error as failure and
session.status with status idle as completion, deduplicating a failed run if
both arrive. Ignore retry status as recoverable work and ignore sessionless
errors. The idle event has no final assistant text. While the session runs,
use message.updated only to cache an assistant message’s messageID and
sessionID. For a correlated message.part.updated, retain only a sanitized,
240-character excerpt when part.type is text; ignore reasoning, tool,
synthetic, and ignored parts. Consume and delete the excerpt at idle. Never
retain the full text or fetch session history to manufacture an excerpt.
Current OpenCode source defines question.asked, including human-facing
question fields, but the public plugin event list does not yet name it. Verify
the installed version first. If it is unavailable, use the intercepting
tool.execute.before hook for the built-in question tool only as an
immediate-return, fail-open fallback; enable exactly one question path and
never await network delivery or change the tool call. Ignore child sessions
unless the user explicitly wants subagent alerts: cache whether info.parentID
is present from session.created and session.updated, then clear that entry on
session.deleted. OpenCode does not await event-handler promises, so catch
every sender failure inside the plugin; a one-shot process may exit before an
asynchronous send finishes.
The installation is proven only after all checks pass:
- The local installer or test mode sends a direct request, reveals only the
HTTP status, and receives
202 Accepted. - Reload or restart the agent when required. In Codex, open
/hooksand trust the new user hook before testing it. - Complete a trivial turn and confirm that the completion event produces one notification. When the event provides a final message, confirm the notification includes a useful excerpt.
- For an agent with a documented permission event, trigger a real permission prompt and confirm that it produces one notification while the agent is waiting, with enough context to know what it needs.
- Test the clarification path: make Codex or Pi end a response with a question,
make Claude Code call
AskUserQuestion, make Copilot CLI open anelicitation_dialog, or make OpenCode call itsquestiontool. Confirm that the resulting notification includes the question or request. Do not count an ended-turn question as proof of a live structured-input alert. - In Claude Code, enter plan mode and confirm that
ExitPlanModeproduces one approval notification, even if an overlappingpermission_promptalso fires. - In Claude Code, trigger a terminal API failure and confirm that
StopFailureproduces one failure notification. In Copilot CLI, trigger a nonrecoverable execution error and confirm that it produces one needs-attention notification, without a duplicate ifagentStopfollows. In Pi, trigger a terminal model error and confirm that the eventualagent_settledalert is sent once after retries stop. In OpenCode, confirm asession.erroralert is not duplicated if the same session then reportssession.statusasidle. - With full notification content available and iOS Notification Grouping
enabled, trigger two different lifecycle events for one task and confirm
Notification Center groups them together. Start a different task and confirm
it uses a different group. Inspect the request in local test mode and confirm
only the opaque derived
thread_id, never the raw task or session identifier, is sent. - Make the secret unavailable or make the sender fail, then repeat a completion and permission prompt. The agent must behave exactly as it did without CC’ed: no block, approval, denial, continuation, or injected context.
- Confirm every unrelated pre-existing hook still runs. Running the sender manually does not prove that the agent loaded every event.
Choose which devices receive it
#The ready-made command on the Home screen defaults to All devices. To target only the device you are holding, choose This device from the route selector above the command.
| Route | Use it when |
|---|---|
| All devices | You want the same notification on every device connected to CC’ed. |
| This device | A notification belongs on only the device you are holding. |
After choosing a route, open Copy. Choose Copy code for a command you can run immediately, Copy URL for an integration, or Copy Secret when a tool needs the secret separately.
Treat every webhook URL like a password. Anyone who has it can send notifications to the devices it addresses.
Useful examples
#For the examples below, choose All devices or This device from the route selector on the Home screen, then tap Copy → Copy URL. In Terminal, read it into a temporary variable:
printf 'Webhook URL: '
IFS= read -rs CCED_WEBHOOK_URL
printf '\n'
export CCED_WEBHOOK_URL
The URL stays out of the command itself and disappears when you close that Terminal session.
Send a simple message
#For a plain notification, the request body is the message:
curl --fail-with-body -X POST "$CCED_WEBHOOK_URL" \
-d 'The nightly backup finished.'
Add a title and group related notifications
#Send JSON when you want more control. A thread_id keeps related notifications
together in Notification Center:
curl --fail-with-body -X POST "$CCED_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d '{
"title": "Production deploy",
"message": "Version 2.4.0 is live",
"thread_id": "production-deploys",
"sound": "default"
}'
Send a Time Sensitive notification
#For something that needs attention now, set interruption_level to
time-sensitive:
curl --fail-with-body -X POST "$CCED_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d '{
"title": "Production alert",
"message": "The checkout service is unavailable",
"interruption_level": "time-sensitive"
}'
Time Sensitive notifications can appear immediately during Focus and Notification Summary, but the user remains in control. On iPhone and iPad, open Settings → Focus → [Focus] → Apps, then allow CC’ed or turn on Time Sensitive Notifications. On Mac, open System Settings → Focus → [Focus] → Allowed Apps. The per-app control is under CC’ed’s notification settings on each platform. The first Time Sensitive notification prompts the system to explain this behavior and let the user turn it off. Reserve this level for events that need immediate attention; CC’ed does not mark ordinary notifications as Time Sensitive.
Send from Node.js
#This standalone example requires Node.js 18 or later. Never put a webhook URL in browser JavaScript, where visitors could read it. Store the URL in an environment variable instead of source code:
async function sendNotification() {
const response = await fetch(process.env.CCED_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: "Coffee machine",
message: "The water tank is empty",
}),
});
if (!response.ok) throw new Error(`CC'ed returned ${response.status}`);
}
sendNotification().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Test from GitHub Actions
#Add the complete webhook URL as a repository secret named
CCED_WEBHOOK_URL. This manual workflow confirms the secret works before you
add the same step after a real build or deploy job:
name: Test CC'ed notification
on:
workflow_dispatch:
jobs:
send-test:
runs-on: ubuntu-latest
steps:
- name: Send notification
env:
CCED_WEBHOOK_URL: ${{ secrets.CCED_WEBHOOK_URL }}
run: |
curl --fail-with-body -X POST "$CCED_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d '{"title":"GitHub Actions","message":"The deploy finished successfully."}'
API reference
#The examples above cover the common case. Use this section when you are building an integration or handling errors.
Endpoints
#| Method | Route | Use |
|---|---|---|
POST |
/v1/{webhookSecret} |
Send plain text or JSON with the secret in the URL. |
POST |
/v1/send |
Send the secret as Authorization: Bearer …. |
GET |
/v1/{webhookSecret} |
Send fields as query parameters when POST is unavailable; message is required. |
Prefer POST. A GET request can expose both the secret and message in browser,
proxy, and access logs.
Notification fields
#| Field | What it does |
|---|---|
message |
Required. The main notification text. |
title |
Adds a title above the message. |
subtitle |
Adds a second line below the title. |
thread_id |
Groups related notifications in Notification Center. |
sound |
Use default or system with the current app. |
expiration_date |
Tells APNs when it should stop retrying delivery. Use an RFC 3339 timestamp. |
interruption_level |
passive, active, or time-sensitive. Time Sensitive notifications can break through Focus when the user allows them. |
The API also accepts open_url, image_url, and filter_criteria for forward
compatibility. volume is valid only with interruption_level: critical. The
current Apple app can attach supported HTTPS images to notifications, but it
does not open links or enable Focus filters, critical alerts, or custom sound
files. Do not depend on those behaviors yet.
Send JSON with Content-Type: application/json. Without that header, CC’ed
treats the entire body—including JSON-looking text—as the notification message.
Unknown JSON fields are rejected.
Responses
#| Status | Meaning |
|---|---|
202 Accepted |
At least one active, push-registered device was accepted for delivery. |
400 Bad Request |
The JSON is malformed or the text is not valid UTF-8. |
401 Unauthorized |
/v1/send is missing a valid authorization header. |
404 Not Found |
The secret is malformed or unknown, or no active, push-registered device route remains. |
413 Content Too Large |
The HTTP request body is larger than 16 KiB. |
422 Unprocessable Content |
A field or the final APNs payload is invalid. |
503 Service Unavailable |
CC’ed cannot accept the request right now. |
An All devices request can succeed for some devices and fail for others.
Check accepted_installations and failed_installations in the JSON response
when partial delivery matters.
Access controls what a device can reveal, not whether its active route receives a
push. The notification service extension checks the device’s iCloud-synced
complimentary deadline and local RevenueCat entitlement. When neither is active, the device still
receives a notification titled CC'ed with the message Notification received.,
while sender-provided text, routing labels, links, and images are omitted from
presentation and local history. A
202 response intentionally discloses no device access state. Every APNs
attempt carries the same generic visible alert and private content envelope;
the receiving device decides whether to reveal the envelope.
A successful response looks like this:
{
"success": true,
"request_id": "6f0d4c91-c3fa-47e7-aea2-ceeb0fc8cfd1",
"accepted_installations": 1,
"failed_installations": 0,
"delivery_mode": "apns"
}
Limits
#message,title, andsubtitle: up to 2,000 characters each.thread_id: up to 256 bytes.- Request body: up to 16 KiB.
- Final APNs payload: up to 4,096 bytes.
- One webhook: up to 64 active, push-registered devices.
The machine-readable OpenAPI schema is provisional. This page is the source of truth for shipped behavior.
Security and privacy
#Keep the URL private
#Do not put a webhook URL in source code, screenshots, or logs. Store it in the secret manager provided by your deployment platform or automation tool.
Some systems record every request path. For those systems, choose Copy
Secret, read it into a temporary variable, then use POST /v1/send with an
authorization header:
printf 'Webhook secret: '
IFS= read -rs WEBHOOK_SECRET
printf '\n'
curl --fail-with-body -X POST 'https://api.cced.app/v1/send' \
-H "Authorization: Bearer $WEBHOOK_SECRET" \
-d 'The garage door is still open.'
Notification contents pass through memory while CC’ed sends them. They are not written to the server database or normal application logs. Notification history stays on your device.
If a notification does not appear
#- The command returned
202. CC’ed accepted the request. Check notification permission, Focus settings, and the device’s network connection. - The command returned
404. The secret is malformed or unknown, or no active, push-registered device route remains. Copy the URL again and confirm push registration is active in the app. - The command returned
422. A field is invalid or the notification is too large. Start with a plain message, then add fields back one at a time. - The command returned
503. Delivery is temporarily unavailable. Try again shortly.
Still stuck? Email support.