Getting Started

Send a notification to your Apple Watch with a single API call.

1. Get Your Device ID

Open the SendBag app on your Apple Watch and go to the Settings page to find your device ID.

2. Send a Notification

curl -X POST https://api.sendbag.cc/notifications \
  -H "Content-Type: application/json" \
  -d '{
    "deviceId": "YOUR_DEVICE_ID",
    "title": "Hello",
    "body": "Your first notification!"
  }'

The notification will appear on your Apple Watch within seconds.

You can also use our Send page to quickly test notifications from your browser.

Encryption

SendBag supports two encryption modes:

Server-Side Encryption (Default)

When plain=true (or omitted), you send plaintext and our server encrypts it before delivery. This is the simplest option and works great for most use cases.

End-to-End Encryption

When plain=false, you encrypt the message yourself before sending. Our server never sees the plaintext content.

How it works:

  1. Fetch the device's public key:
    GET https://api.sendbag.cc/notifications/users/{deviceId}/public-key
  2. Generate a random 256-bit AES key
  3. Encrypt each field (title, subtitle, body) with AES-256-GCM:
    • Use a random 12-byte nonce per field
    • Format: Base64(nonce || ciphertext || tag)
  4. Encrypt the AES key with RSA-OAEP-SHA256 using the device's public key
  5. Send with plain=false and include encryptedKey

Code Examples

Complete examples for sending E2E encrypted notifications in various languages:

import base64
import os
import requests
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes

DEVICE_ID = "YOUR_DEVICE_ID"
API_BASE = "https://api.sendbag.cc"

def encrypt_field(plaintext: str, aes_key: bytes) -> str:
    """Encrypt a field with AES-256-GCM. Returns Base64(nonce || ciphertext || tag)"""
    nonce = os.urandom(12)
    aesgcm = AESGCM(aes_key)
    ciphertext_with_tag = aesgcm.encrypt(nonce, plaintext.encode(), None)
    return base64.b64encode(nonce + ciphertext_with_tag).decode()

def send_encrypted_notification(title: str, body: str, subtitle: str = ""):
    # 1. Fetch public key
    resp = requests.get(f"{API_BASE}/api/notifications/users/{DEVICE_ID}/public-key")
    public_key_b64 = resp.json()["publicKey"]
    public_key_der = base64.b64decode(public_key_b64)
    public_key = serialization.load_der_public_key(public_key_der)

    # 2. Generate AES key
    aes_key = os.urandom(32)

    # 3. Encrypt fields with AES-GCM
    encrypted_title = encrypt_field(title, aes_key)
    encrypted_subtitle = encrypt_field(subtitle, aes_key)
    encrypted_body = encrypt_field(body, aes_key)

    # 4. Encrypt AES key with RSA-OAEP-SHA256
    encrypted_key = public_key.encrypt(
        aes_key,
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None
        )
    )

    # 5. Send notification
    requests.post(f"{API_BASE}/api/notifications", json={
        "deviceId": DEVICE_ID,
        "title": encrypted_title,
        "subtitle": encrypted_subtitle,
        "body": encrypted_body,
        "plain": False,
        "encryptedKey": base64.b64encode(encrypted_key).decode()
    })

send_encrypted_notification("Hello", "This is E2E encrypted!")
const crypto = require('crypto');

const DEVICE_ID = 'YOUR_DEVICE_ID';
const API_BASE = 'https://api.sendbag.cc';

function encryptField(plaintext, aesKey) {
  // Generate 12-byte nonce
  const nonce = crypto.randomBytes(12);
  // Encrypt with AES-256-GCM
  const cipher = crypto.createCipheriv('aes-256-gcm', aesKey, nonce);
  const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
  const tag = cipher.getAuthTag();
  // Format: nonce || ciphertext || tag
  return Buffer.concat([nonce, ciphertext, tag]).toString('base64');
}

async function sendEncryptedNotification(title, body, subtitle = '') {
  // 1. Fetch public key
  const keyResp = await fetch(`${API_BASE}/api/notifications/users/${DEVICE_ID}/public-key`);
  const { publicKey: publicKeyB64 } = await keyResp.json();
  const publicKeyDer = Buffer.from(publicKeyB64, 'base64');

  // 2. Generate AES key
  const aesKey = crypto.randomBytes(32);

  // 3. Encrypt fields with AES-GCM
  const encryptedTitle = encryptField(title, aesKey);
  const encryptedSubtitle = encryptField(subtitle, aesKey);
  const encryptedBody = encryptField(body, aesKey);

  // 4. Encrypt AES key with RSA-OAEP-SHA256
  const publicKey = crypto.createPublicKey({ key: publicKeyDer, format: 'der', type: 'spki' });
  const encryptedKey = crypto.publicEncrypt(
    { key: publicKey, oaepHash: 'sha256', padding: crypto.constants.RSA_PKCS1_OAEP_PADDING },
    aesKey
  ).toString('base64');

  // 5. Send notification
  await fetch(`${API_BASE}/api/notifications`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      deviceId: DEVICE_ID,
      title: encryptedTitle,
      subtitle: encryptedSubtitle,
      body: encryptedBody,
      plain: false,
      encryptedKey
    })
  });
}

sendEncryptedNotification('Hello', 'This is E2E encrypted!');
package main

import (
    "bytes"
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "crypto/rsa"
    "crypto/sha256"
    "crypto/x509"
    "encoding/base64"
    "encoding/json"
    "io"
    "net/http"
)

const (
    deviceID = "YOUR_DEVICE_ID"
    apiBase  = "https://api.sendbag.cc"
)

func encryptField(plaintext string, aesKey []byte) (string, error) {
    block, _ := aes.NewCipher(aesKey)
    gcm, _ := cipher.NewGCM(block)

    nonce := make([]byte, 12)
    io.ReadFull(rand.Reader, nonce)

    ciphertext := gcm.Seal(nil, nonce, []byte(plaintext), nil)
    // Format: nonce || ciphertext || tag (tag is appended by Seal)
    combined := append(nonce, ciphertext...)
    return base64.StdEncoding.EncodeToString(combined), nil
}

func sendEncryptedNotification(title, body, subtitle string) error {
    // 1. Fetch public key
    resp, _ := http.Get(apiBase + "/api/notifications/users/" + deviceID + "/public-key")
    defer resp.Body.Close()
    var keyData struct{ PublicKey string `json:"publicKey"` }
    json.NewDecoder(resp.Body).Decode(&keyData)
    publicKeyDer, _ := base64.StdEncoding.DecodeString(keyData.PublicKey)
    pubKey, _ := x509.ParsePKIXPublicKey(publicKeyDer)
    rsaPubKey := pubKey.(*rsa.PublicKey)

    // 2. Generate AES key
    aesKey := make([]byte, 32)
    io.ReadFull(rand.Reader, aesKey)

    // 3. Encrypt fields with AES-GCM
    encTitle, _ := encryptField(title, aesKey)
    encSubtitle, _ := encryptField(subtitle, aesKey)
    encBody, _ := encryptField(body, aesKey)

    // 4. Encrypt AES key with RSA-OAEP-SHA256
    encKey, _ := rsa.EncryptOAEP(sha256.New(), rand.Reader, rsaPubKey, aesKey, nil)

    // 5. Send notification
    payload, _ := json.Marshal(map[string]interface{}{
        "deviceId": deviceID, "title": encTitle, "subtitle": encSubtitle,
        "body": encBody, "plain": false,
        "encryptedKey": base64.StdEncoding.EncodeToString(encKey),
    })
    http.Post(apiBase+"/api/notifications", "application/json", bytes.NewReader(payload))
    return nil
}
import Foundation
import CryptoKit

let deviceId = "YOUR_DEVICE_ID"
let apiBase = "https://api.sendbag.cc"

func encryptField(_ plaintext: String, key: SymmetricKey) throws -> String {
    let nonce = AES.GCM.Nonce()
    let data = Data(plaintext.utf8)
    let sealedBox = try AES.GCM.seal(data, using: key, nonce: nonce)
    // Format: nonce || ciphertext || tag
    let combined = nonce + sealedBox.ciphertext + sealedBox.tag
    return combined.base64EncodedString()
}

func sendEncryptedNotification(title: String, body: String, subtitle: String = "") async throws {
    // 1. Fetch public key
    let keyURL = URL(string: "\(apiBase)/api/notifications/users/\(deviceId)/public-key")!
    let (keyData, _) = try await URLSession.shared.data(from: keyURL)
    let keyJson = try JSONDecoder().decode([String: String].self, from: keyData)
    let publicKeyData = Data(base64Encoded: keyJson["publicKey"]!)!

    // Import RSA public key
    var error: Unmanaged?
    let publicKey = SecKeyCreateWithData(
        publicKeyData as CFData,
        [kSecAttrKeyType: kSecAttrKeyTypeRSA, kSecAttrKeyClass: kSecAttrKeyClassPublic] as CFDictionary,
        &error
    )!

    // 2. Generate AES key
    let aesKey = SymmetricKey(size: .bits256)

    // 3. Encrypt fields with AES-GCM
    let encTitle = try encryptField(title, key: aesKey)
    let encSubtitle = try encryptField(subtitle, key: aesKey)
    let encBody = try encryptField(body, key: aesKey)

    // 4. Encrypt AES key with RSA-OAEP-SHA256
    let aesKeyData = aesKey.withUnsafeBytes { Data($0) }
    let encryptedKey = SecKeyCreateEncryptedData(
        publicKey, .rsaEncryptionOAEPSHA256, aesKeyData as CFData, &error
    )! as Data

    // 5. Send notification
    var request = URLRequest(url: URL(string: "\(apiBase)/api/notifications")!)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONEncoder().encode([
        "deviceId": deviceId, "title": encTitle, "subtitle": encSubtitle,
        "body": encBody, "plain": "false", "encryptedKey": encryptedKey.base64EncodedString()
    ])
    try await URLSession.shared.data(for: request)
}
import java.security.KeyFactory
import java.security.spec.X509EncodedKeySpec
import java.util.Base64
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.spec.GCMParameterSpec
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject

const val DEVICE_ID = "YOUR_DEVICE_ID"
const val API_BASE = "https://api.sendbag.cc"

fun encryptField(plaintext: String, aesKey: javax.crypto.SecretKey): String {
    val nonce = ByteArray(12).apply { java.security.SecureRandom().nextBytes(this) }
    val cipher = Cipher.getInstance("AES/GCM/NoPadding").apply {
        init(Cipher.ENCRYPT_MODE, aesKey, GCMParameterSpec(128, nonce))
    }
    val ciphertext = cipher.doFinal(plaintext.toByteArray())
    // Format: nonce || ciphertext (includes tag)
    return Base64.getEncoder().encodeToString(nonce + ciphertext)
}

fun sendEncryptedNotification(title: String, body: String, subtitle: String = "") {
    val client = OkHttpClient()

    // 1. Fetch public key
    val keyResp = client.newCall(
        Request.Builder().url("$API_BASE/api/notifications/users/$DEVICE_ID/public-key").build()
    ).execute()
    val publicKeyB64 = JSONObject(keyResp.body!!.string()).getString("publicKey")
    val publicKeyDer = Base64.getDecoder().decode(publicKeyB64)
    val publicKey = KeyFactory.getInstance("RSA")
        .generatePublic(X509EncodedKeySpec(publicKeyDer))

    // 2. Generate AES key
    val aesKey = KeyGenerator.getInstance("AES").apply { init(256) }.generateKey()

    // 3. Encrypt fields with AES-GCM
    val encTitle = encryptField(title, aesKey)
    val encSubtitle = encryptField(subtitle, aesKey)
    val encBody = encryptField(body, aesKey)

    // 4. Encrypt AES key with RSA-OAEP-SHA256
    val rsaCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding").apply {
        init(Cipher.ENCRYPT_MODE, publicKey)
    }
    val encryptedKey = Base64.getEncoder().encodeToString(rsaCipher.doFinal(aesKey.encoded))

    // 5. Send notification
    val json = JSONObject(mapOf(
        "deviceId" to DEVICE_ID, "title" to encTitle, "subtitle" to encSubtitle,
        "body" to encBody, "plain" to false, "encryptedKey" to encryptedKey
    )).toString()
    client.newCall(Request.Builder()
        .url("$API_BASE/api/notifications")
        .post(json.toRequestBody("application/json".toMediaType()))
        .build()
    ).execute()
}

API Reference

Send Notification

POST https://api.sendbag.cc/notifications
Field Type Required Description
deviceId string Yes Your device ID from the SendBag app
title string No Notification title (displayed prominently). Max 1024 bytes.
subtitle string No Notification subtitle. Max 1024 bytes.
body string No Notification body text. Max 1024 bytes.
plain boolean No Encryption mode. Default: true (server-side encryption). Set to false for E2E encryption.
encryptedKey string No* RSA-encrypted AES key. *Required when plain=false.

Response

Returns HTTP 200 with an empty body on success.

Get Public Key

GET https://api.sendbag.cc/notifications/users/{deviceId}/public-key

Retrieve a device's RSA public key for E2E encryption.

Response:

{
  "deviceId": "YOUR_DEVICE_ID",
  "publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A..."
}

The public key is Base64-encoded X.509 SPKI DER format.