Docs
Everything you need to get messages onto your Even Realities G2: pair it, send a ping, wire up your tools.
Quickstart
- Install EyePush from Even Hub (Even Realities app, then Even Hub) and open it on your glasses. It shows a pairing code.
- Go to eyepush.app/pair and enter the code.
- Open the dashboard, create a source, and copy its key. Keys are 20 lowercase alphanumeric characters. Then send a ping:
curl -X POST https://eyepush.app/h/YOUR_KEY \
-H "Content-Type: application/json" \
-d '{"title":"Hello","body":"It works."}'
A source is one named inbound channel, such as “GitHub”, “Home” or “Claude”. Each has its own webhook URL, email address and MCP URL, and you can revoke one without breaking the others. Treat the key like a password: anyone who has it can send you pings.
Widget on the G2
The EyePush widget shows a live feed on the lens. Open it from the glasses after installing it from Even Hub. Each ping shows up to 2,000 characters; emoji and characters outside the G2 font are dropped by the glasses.
| Gesture | Action |
|---|---|
| Tap | Jump to the latest ping, or to a question waiting for an answer |
| Scroll (temple touchpad) | Browse history |
| Double-tap | Exit the widget |
| Tap on a question | Answer it: scroll to switch between the two answers, tap to send, double-tap to back out |
The R1 ring’s tap and swipe work the same way.
The status line reads like LIVE · 1/12 · 09:41: feed state, position in history, and the time.
Phone notifications
EyePush also sends web push notifications to your phone. The G2 mirrors phone notifications onto the lens through its built-in notification mirroring, so pings reach you even when the widget is closed.
- Open eyepush.app/app on your phone and tap Enable notifications.
iPhone: tap Share, then Add to Home Screen first, and open EyePush from the Home Screen icon. This needs iOS 16.4 or later. - Send a test notification from the dashboard.
- In the Even Realities app, go to Settings, then Notifications, and enable EyePush (or your browser).
Notifications only show on the glasses for apps that have sent at least one notification. That is why the test in step 2 comes before step 3.
Add "silent": true to a ping to keep it on the widget only, with no phone notification.
Webhook
POST https://eyepush.app/h/YOUR_KEY
{
"title": "Deploy finished",
"body": "api-server v2.4.1 is live",
"silent": false
}
| Field | Notes |
|---|---|
body | The message. Aliases: text, message, content. |
title | Optional heading. Alias: subject. |
silent | true sends to the widget only, with no phone notification. |
Accepted formats
- JSON, as above.
- Plain text: the whole request body becomes the message body.
- Form-encoded fields with the same names.
- Slack incoming-webhook payloads (
text,blocks,attachments) and Discord webhook payloads (content,embeds), as-is. Any tool that can post to a Slack or Discord webhook can post to EyePush: swap the URL. - GitHub webhooks (content type
application/json) are formatted automatically forpush,pull_request,issues,releaseandworkflow_runevents. - Anything else: unrecognized JSON is shown as
key: valuelines.
Responses
HTTP/1.1 202 Accepted
{"id":"…","widgets":1,"phones":1}
widgets and phones count the paired glasses and phones the ping was delivered to. See Errors for the rest.
GET requests
For tools that can only open a URL (IFTTT, iOS Shortcuts, bookmarks), the same endpoint accepts a query string.
https://eyepush.app/h/YOUR_KEY?title=Hi&body=Hello
URL-encode the values. Add &silent=true for widget-only.
REST API
Prefer a header over a key in the URL? Use the authenticated endpoint. The OpenAPI 3.1 spec is at eyepush.app/openapi.json.
curl -X POST https://eyepush.app/api/v1/messages \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Hello","body":"From the API","silent":false}'
Send to YOUR_KEY@eyepush.app. The subject becomes the title and the plain-text body becomes the body. Quoted replies and signatures are trimmed, and attachments are ignored. It’s a good fit for forwarding rules and tools that can only send email.
Slack
- Webhook URL: paste your EyePush URL anywhere a tool asks for a Slack incoming-webhook URL.
- Slash command and shortcut: in the dashboard, open Sources, then Slack, for a one-click Slack app manifest. It adds a
/glasses <message>slash command and a “Send to glasses” message shortcut to your workspace. In Slack: Create app, From manifest, paste, Install.
Claude, ChatGPT and MCP
EyePush runs a remote MCP server at https://eyepush.app/mcp/YOUR_KEY (Streamable HTTP). There is no OAuth: the key in the URL is the secret.
| Tool | Arguments | What it does |
|---|---|---|
send_to_glasses | body, optional title and silent | Shows a message. |
ask_on_glasses | question, optional details, yes, no and expires_in_minutes | Asks a yes/no question and waits up to 45 seconds for the answer. |
check_answer | id | Keeps waiting on a question that wasn’t answered in time. |
Tell your agent when to use them, for example: “When the tests pass, ask me on my glasses before you push to main.”
Claude Code
claude mcp add --transport http eyepush https://eyepush.app/mcp/YOUR_KEY
Claude (web and desktop)
Settings, then Connectors, then Add custom connector, and paste the URL.
ChatGPT
Enable developer mode, then add a custom connector (MCP) with the URL and no authentication.
Custom GPTs
Configure, then Actions, then Import from URL: https://eyepush.app/openapi.json. Set Authentication to API key, Auth type to Bearer, and paste your key.
Any other agent framework or automation can simply POST to the webhook.
Yes/no questions
Add ask to a ping and it becomes a question. You answer on the lens, with a tap on the glasses or the R1 ring, and the sender gets the answer back. Use it as an approval step: deploy or not, merge or not, spend or not.
curl -X POST https://eyepush.app/h/YOUR_KEY \
-H "Content-Type: application/json" \
-d '{"ask":"Run the migration on prod?","body":"3 tables, about 40s of downtime","yes":"Approve","no":"Deny","wait":60}'
| Field | Notes |
|---|---|
ask | The question, or true to ask the title or body. |
yes, no | Labels for the two answers, up to 20 characters each. Default Yes and No. |
expiresIn | Seconds the question stays open, from 10 to 604,800 (a week). Default 3,600. |
wait | Seconds to hold the request open for the answer, up to 60. Default 0. |
callbackUrl | An https:// URL that receives the outcome. See Callbacks. |
The fields work as form fields and query parameters too: https://eyepush.app/h/YOUR_KEY?ask=Deploy%3F&yes=Ship&no=Hold. With the REST API, POST {"question": "…"} to /api/v1/questions.
Getting the answer
The response describes the question. With wait, it returns as soon as you answer:
HTTP/1.1 202 Accepted
{
"id": "0mvsnkqx9yhfef4",
"status": "answered",
"answer": "yes",
"choice": "Approve",
"title": "Run the migration on prod?",
"body": "3 tables, about 40s of downtime",
"askedAt": 1793224514637,
"answeredAt": 1793224517529,
"expiresAt": 1793228114635,
"widgets": 1,
"phones": 1,
"pollUrl": "https://eyepush.app/h/YOUR_KEY/questions/0mvsnkqx9yhfef4"
}
status is open, answered or expired. answer is yes, no or null, and choice is the label that was picked. Times are milliseconds since the epoch. While a question is open, GET its pollUrl, adding ?wait=60 to wait for a change. The first answer is final.
Answering
- On the lens. A new question takes over the lens. Tap to answer, scroll to switch between the two answers, and tap again to send. Double-tap backs out without answering.
- On the phone. The widget’s screen in the Even Realities app and the dashboard both show the two answers as buttons. On Android and desktop, the push notification has them too.
Callbacks
With callbackUrl, EyePush POSTs the same JSON there when the question is answered or expires, with "event" set to "question.answered" or "question.expired". If your server is down or returns a 5xx, it tries twice more. The X-EyePush-Signature header is sha256= followed by the hex HMAC-SHA256 of the raw body, keyed with the source key the question was asked with. Check it before acting on a yes:
import { createHmac, timingSafeEqual } from "node:crypto";
function fromEyePush(rawBody, signatureHeader, sourceKey) {
const expected = Buffer.from("sha256=" + createHmac("sha256", sourceKey).update(rawBody).digest("hex"));
const given = Buffer.from(signatureHeader ?? "");
return given.length === expected.length && timingSafeEqual(given, expected);
}
Treat expired, timeouts and errors as “no”. Anyone with the source key can ask questions and read their answers, so keep keys used for approvals in a secret store.
Copy-paste recipes
curl
Plain text works too: the whole body becomes the message.
curl -d "Backup finished" https://eyepush.app/h/YOUR_KEY
GitHub Actions
Store your full webhook URL as a repo secret named EYEPUSH_URL.
- name: Ping my glasses
if: always()
run: |
curl -fsS -X POST "${{ secrets.EYEPUSH_URL }}" \
-H "Content-Type: application/json" \
-d "{\"title\":\"${{ github.workflow }}: ${{ job.status }}\",\"body\":\"${{ github.repository }}@${{ github.ref_name }}\"}"Or skip the step entirely: add your EyePush URL as a repository webhook (content type application/json) and push, pull request, issue, release and workflow run events are formatted for you.
Approval gate (shell or CI)
Asks, then keeps waiting until the question is answered or expires. The script fails unless the answer is yes, so put it before the step it guards. It needs curl and jq, which GitHub-hosted runners have.
r=$(curl -fsS "$EYEPUSH_URL" -H "Content-Type: application/json" \
-d '{"ask":"Deploy api to prod?","yes":"Deploy","no":"Hold","expiresIn":900,"wait":60}')
poll=$(echo "$r" | jq -r .pollUrl)
while [ "$(echo "$r" | jq -r .status)" = open ]; do
r=$(curl -fsS "$poll?wait=60")
done
test "$(echo "$r" | jq -r .answer)" = yes
Home Assistant
Add to configuration.yaml, then call rest_command.eyepush from an automation.
rest_command:
eyepush:
url: "https://eyepush.app/h/YOUR_KEY"
method: POST
content_type: "application/json"
payload: '{"title": "{{ title }}", "body": "{{ body }}"}'
# in an automation action:
- service: rest_command.eyepush
data:
title: "Front door"
body: "Opened at {{ now().strftime('%H:%M') }}"
Zapier
Add the Webhooks by Zapier action, event POST. URL: your EyePush URL. Payload Type: json. Data: map title and body from earlier steps. Or paste a Payload JSON like:
{"title":"New lead","body":"Ada Lovelace signed up"}
Make
Add the HTTP, Make a request module. URL: your EyePush URL. Method: POST. Body type: Raw, content type JSON.
{"title":"Scenario finished","body":"12 rows processed"}
n8n
Add an HTTP Request node. Method: POST. URL: your EyePush URL. Send Body: on, Body Content Type: JSON, then specify:
{"title":"Workflow done","body":"{{ $json.summary }}"}
IFTTT
Use the Webhooks, Make a web request action. Method: GET (or POST), content type application/json. Put the values in the query string:
https://eyepush.app/h/YOUR_KEY?title={{ExtraData}}&body={{OccurredAt}}
iOS Shortcuts
Add a Get Contents of URL action. URL: your EyePush URL. Method: POST. Request Body: JSON, with text fields title and body. For a quick version, use GET with a query string instead.
Python
import requests
requests.post(
"https://eyepush.app/h/YOUR_KEY",
json={"title": "Job done", "body": "Trained 3 epochs", "silent": False},
timeout=10,
)
JavaScript
await fetch("https://eyepush.app/h/YOUR_KEY", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "Build done", body: "All 214 tests passed" }),
});
Limits
| Free | Pro | |
|---|---|---|
| Sources | 2 | 25 |
| Pings per day | 100 | 5,000 |
| History | Last 50 messages | Last 1,000 messages |
| URL watchers | - | Up to 10, polled every 5 minutes |
| Questions | Count as pings. Open from 10 seconds to a week; kept past the history limit until answered or expired. | |
Daily counters reset at 00:00 UTC. Request bodies are limited to 64 KB, and the widget shows up to 2,000 characters per ping.
URL watchers (Pro) poll any URL or JSON API every 5 minutes and ping you when the value changes.
Delivery is best-effort. Do not use EyePush for emergency, medical or safety-critical alerts.
Errors
| Status | Meaning |
|---|---|
202 | Accepted. Body: {"id":"…","widgets":1,"phones":1}, plus the question fields for a question. |
400 | Invalid question: no text, or a callbackUrl that isn’t https://. |
402 | The source is paused because the account is over its plan’s source limit. |
404 | Unknown key or question. Check it, or it may have been revoked. |
409 | Answering a question that was already answered or has expired. |
413 | Request body over 64 KB. |
429 | Daily limit reached. Resets at 00:00 UTC. |
Stuck? Email hello@eyepush.app.