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 and PermissionRequest when approval is actually required. For Claude Code, use Stop, StopFailure, Notification for permission_prompt|elicitation_dialog, and asynchronous PreToolUse for AskUserQuestion|ExitPlanMode. Add agent_needs_input only when the installed Claude version supports it and you can prevent duplicate alerts. For GitHub Copilot CLI, use agentStop, notification for permission_prompt|elicitation_dialog, and errorOccurred only when recoverable is false. For Pi, inspect the latest agent_end messages immediately, retain only a sanitized bounded final excerpt and failure classification, then send once on agent_settled, after automatic retries, compaction, and queued follow-ups have stopped. For OpenCode, use session.status when its status is idle, session.error, permission.asked, and question.asked when the installed version exposes it. Otherwise use tool.execute.before when the tool is question, but never both question paths. Retain only a sanitized, bounded assistant excerpt until the session becomes idle: use message.updated to identify assistant message IDs, then accept only correlated message.part.updated content whose part.type is text, 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 with POST https://api.cced.app/v1/send and Authorization: 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 when background_tasks and session_crons are 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’ed thread_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, omit thread_id rather 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-side Notification and PreToolUse hooks.

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:

  1. The local installer or test mode sends a direct request, reveals only the HTTP status, and receives 202 Accepted.
  2. Reload or restart the agent when required. In Codex, open /hooks and trust the new user hook before testing it.
  3. 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.
  4. 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.
  5. Test the clarification path: make Codex or Pi end a response with a question, make Claude Code call AskUserQuestion, make Copilot CLI open an elicitation_dialog, or make OpenCode call its question tool. 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.
  6. In Claude Code, enter plan mode and confirm that ExitPlanMode produces one approval notification, even if an overlapping permission_prompt also fires.
  7. In Claude Code, trigger a terminal API failure and confirm that StopFailure produces one failure notification. In Copilot CLI, trigger a nonrecoverable execution error and confirm that it produces one needs-attention notification, without a duplicate if agentStop follows. In Pi, trigger a terminal model error and confirm that the eventual agent_settled alert is sent once after retries stop. In OpenCode, confirm a session.error alert is not duplicated if the same session then reports session.status as idle.
  8. 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.
  9. 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.
  10. 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:

Shell
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:

Shell
curl --fail-with-body -X POST "$CCED_WEBHOOK_URL" \
  -d 'The nightly backup finished.'
#

Send JSON when you want more control. A thread_id keeps related notifications together in Notification Center:

Shell
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:

Shell
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:

JAVASCRIPT
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:

YAML
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:

JSON
{
  "success": true,
  "request_id": "6f0d4c91-c3fa-47e7-aea2-ceeb0fc8cfd1",
  "accepted_installations": 1,
  "failed_installations": 0,
  "delivery_mode": "apns"
}

Limits

#

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:

Shell
printf 'Webhook secret: '
IFS= read -rs WEBHOOK_SECRET
printf '\n'
Shell
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

#

Still stuck? Email support.