API

A way to programmatically update your vault's contents — for technical people who want to automatically store the data they need kept safe, just in case.

🛈 The API is completely optional. Most people will never need it — the website handles everything you need to create and manage your vault. This page is for developers and technical users who want to automate vault updates from scripts or servers.

Why use the API?

IfGone provides a REST API so you can automate vault updates from scripts, servers, and scheduled tasks. Whether it's backing up a manuscript you're working on, saving the latest draft of important instructions, or keeping your emergency contacts up to date — your systems can update vault slots without opening a browser.

Your vault has 10 encrypted slots (0–9), each holding up to 10 MB. Each slot can be updated once per month. The server never sees your plaintext: you encrypt data locally using your account's public key, then upload the ciphertext. Every request is signed with your private key so the server knows it came from you.

Authentication

Every write request must be signed with your ECDSA-SHA256 private key (P-256 curve). The signature proves the request came from you without transmitting the private key.

Signing process

  1. Serialize your request body as a JSON string
  2. Compute the SHA-256 hash of the body and encode it as lowercase hex
  3. Build the message: {timestamp}:{bodyHash} — where timestamp is a Unix timestamp in seconds (UTC). The timestamp serves as a nonce to prevent replay attacks and must be UTC-based to prevent timezone manipulation.
  4. Sign the message with ECDSA-SHA256 using your P-256 private key
  5. Base64-encode the DER-encoded signature (not raw P1363)

Required headers

HeaderDescription
X-Account-IdYour account ID (e.g. acc_xxxxxxxxxxxx). Required on all authenticated endpoints. Sent as a header, not in the request body.
X-SignatureBase64-encoded DER ECDSA-SHA256 signature
X-TimestampUnix timestamp in seconds, UTC (must be within 5 minutes of server time)

The private key is the same one generated during vault setup and stored on your physical token.

Encryption

Slot data must be encrypted before uploading. IfGone never receives plaintext. The encryption scheme is ECIES (Elliptic Curve Integrated Encryption Scheme) using the P-256 curve:

  1. ECDH key agreement — Generate an ephemeral P-256 key pair. Derive a shared secret between the ephemeral private key and your account's public key.
  2. HKDF-SHA256 key derivation — Derive a 32-byte AES key from the shared secret using HKDF-SHA256 with a random 16-byte salt and info string "ifgone-slot-encryption".
  3. AES-256-GCM encryption — Encrypt the plaintext using AES-256-GCM with a random 12-byte IV. The output is ciphertext concatenated with the 16-byte authentication tag.

Encrypted payload fields

FieldDescription
encryptedDataBase64-encoded AES-256-GCM ciphertext + authentication tag
encryptedDekBase64-encoded ephemeral public key (raw uncompressed P-256 point, 65 bytes starting with 0x04)
ivBase64-encoded 12-byte AES-GCM initialization vector
saltBase64-encoded 16-byte HKDF salt

The account's public key is available from the public account endpoint — no authentication required to fetch it.

🔑 Decryption confirmation: Decryption requires ONLY the private key. The vault stores everything else needed (ephemeral public key, salt, IV). An executor holding a metal plate or printed QR code has everything needed to decrypt vault contents — no other keys, passwords, or server-side data are required.

Endpoints

POST /api/v1/slot/:slotId/update

Overwrites a slot's encrypted contents. slotId must be 0–9. Requires signature. Rate limited to 1 update per slot per 30 days. Account ID is provided via the X-Account-Id header.

Request body
{
  "encryptedData": "base64...",
  "encryptedDek": "base64...",
  "iv": "base64...",
  "salt": "base64..."
}
Response (202 Accepted)
{
  "slotId": 0,
  "sizeBytes": 2048,
  "updatedAt": "2025-06-01T12:00:00.000Z",
  "nextUpdateAvailable": "2025-07-01T12:00:00.000Z"
}

Status codes

CodeMeaningWhen
202AcceptedSlot updated successfully
400Bad RequestInvalid slotId, missing fields, exceeds 10 MB
401UnauthorizedMissing/invalid signature or timestamp
402Payment RequiredAccount not funded
429Too Many RequestsRate limit (1 update per slot per 30 days)
500Server ErrorInternal error
GET /api/v1/slots

Lists all slots with metadata (size, last updated, next update available). Requires X-Account-Id, X-Signature, and X-Timestamp headers.

Required headers
HeaderDescription
X-Account-IdYour account ID
X-SignatureBase64-encoded DER ECDSA-SHA256 signature
X-TimestampUnix timestamp in seconds, UTC (within 5 minutes)
Response (200 OK)
{
  "accountId": "acc_xxxxxxxxxxxx",
  "ownerEmail": "user@example.com",
  "vetoWindowHours": 72,
  "slots": [
    {
      "slotId": 0,
      "sizeBytes": 2048,
      "lastUpdated": "2025-06-01T12:00:00.000Z",
      "nextUpdateAvailable": "2025-07-01T12:00:00.000Z",
      "label": "Server Credentials"
    }
  ]
}

Status codes

CodeMeaningWhen
200OKSlots listed successfully
401UnauthorizedMissing/invalid signature or timestamp
402Payment RequiredAccount not funded
500Server ErrorInternal error
PUT /api/v1/slot/:slotId/label

Updates a slot's display label (max 20 characters). Requires signature. Rate limited to 1 change per slot per 30 days. Account ID is provided via the X-Account-Id header.

Request body
{
  "label": "Server Credentials"
}

Status codes

CodeMeaningWhen
200OKLabel updated successfully
400Bad RequestInvalid slotId, label missing or exceeds 20 characters
401UnauthorizedMissing/invalid signature or timestamp
402Payment RequiredAccount not funded
429Too Many RequestsRate limit (1 label change per slot per 30 days)
500Server ErrorInternal error
POST /api/v1/slot/:slotId/retrieve

Initiates retrieval for a single slot. slotId must be 0–9. Requires X-Account-Id, X-Signature, and X-Timestamp headers.

Required headers
HeaderDescription
X-Account-IdYour account ID
X-SignatureBase64-encoded DER ECDSA-SHA256 signature
X-TimestampUnix timestamp in seconds, UTC (within 5 minutes)
Request body
{
  "executorEmail": "executor@example.com"
}
Response (202 Accepted)
{
  "retrievalId": "ret_xxxxxxxxxxxx",
  "slotId": 0,
  "requestedAt": "2025-06-01T12:00:00.000Z",
  "vetoDeadline": "2025-06-04T12:00:00.000Z",
  "executorEmail": "executor@example.com"
}

Status codes

CodeMeaningWhen
202AcceptedSlot retrieval initiated
400Bad RequestInvalid slotId or missing executor email
401UnauthorizedMissing/invalid signature or timestamp
402Payment RequiredAccount not funded
500Server ErrorInternal error
POST /api/v1/retrieve

Initiates whole-vault retrieval (all slots). Requires X-Account-Id, X-Signature, and X-Timestamp headers.

Required headers
HeaderDescription
X-Account-IdYour account ID
X-SignatureBase64-encoded DER ECDSA-SHA256 signature
X-TimestampUnix timestamp in seconds, UTC (within 5 minutes)
Request body
{
  "executorEmail": "executor@example.com"
}
Response (202 Accepted)
{
  "retrievalId": "ret_xxxxxxxxxxxx",
  "requestedAt": "2025-06-01T12:00:00.000Z",
  "vetoDeadline": "2025-06-04T12:00:00.000Z",
  "executorEmail": "executor@example.com"
}

Status codes

CodeMeaningWhen
202AcceptedVault retrieval initiated
400Bad RequestMissing executor email
401UnauthorizedMissing/invalid signature or timestamp
402Payment RequiredAccount not funded
500Server ErrorInternal error

Account Settings

These endpoints allow you to change your account's owner email address and veto window. Changes require a confirmation flow: after requesting a change, a confirmation email is sent to your current address. You must approve the change within the timeout period for it to take effect.

POST /api/v1/account/change-request

Initiates a change to the account's owner email and/or veto window. Requires a signed request. A confirmation email is sent to the current owner email address — the change must be approved within the timeout period. At least one of ownerEmail or vetoWindowHours must be provided.

Required headers
HeaderDescription
X-Account-IdYour account ID
X-SignatureBase64-encoded DER ECDSA-SHA256 signature
X-TimestampUnix timestamp in seconds, UTC (within 5 minutes)
Request body
FieldTypeRequiredDescription
ownerEmailstringNo*New owner email address. Must be a valid email format.
vetoWindowHoursnumberNo*New veto window in hours. Must be between 48 and 2160.

* At least one of ownerEmail or vetoWindowHours must be provided.

Response (202 Accepted)
{
  "status": "pending",
  "expiresAt": "2025-06-02T12:00:00.000Z"
}

Status codes

CodeMeaningWhen
202AcceptedChange request created, confirmation email sent
400Bad RequestInvalid email format, vetoWindowHours out of range, or no fields provided
401UnauthorizedMissing/invalid signature or timestamp
409ConflictA change request is already pending
500Server ErrorInternal error
GET /api/v1/account/pending-change

Checks whether a settings change is currently pending for the account. Requires a signed request.

Required headers
HeaderDescription
X-Account-IdYour account ID
X-SignatureBase64-encoded DER ECDSA-SHA256 signature
X-TimestampUnix timestamp in seconds, UTC (within 5 minutes)
Response (200 OK — change pending)
{
  "pending": true,
  "ownerEmail": "new@email.com",
  "vetoWindowHours": 168,
  "requestedAt": "2025-06-01T12:00:00.000Z",
  "expiresAt": "2025-06-02T12:00:00.000Z"
}
Response (200 OK — no pending change)
{
  "pending": false
}

Status codes

CodeMeaningWhen
200OKPending change status returned
401UnauthorizedMissing/invalid signature or timestamp
500Server ErrorInternal error
DELETE /api/v1/account/pending-change

Cancels a pending settings change. Requires a signed request.

Required headers
HeaderDescription
X-Account-IdYour account ID
X-SignatureBase64-encoded DER ECDSA-SHA256 signature
X-TimestampUnix timestamp in seconds, UTC (within 5 minutes)
Response (200 OK)
{
  "cancelled": true
}

Status codes

CodeMeaningWhen
200OKPending change cancelled successfully
401UnauthorizedMissing/invalid signature or timestamp
500Server ErrorInternal error
POST /api/v1/account/approve-change

Approves a pending settings change using the token from the confirmation email. This endpoint uses token-based authentication — no signature is required.

Request body
FieldTypeRequiredDescription
tokenstringYesApproval token from the confirmation email
Response (200 OK)
{
  "accountId": "acc_xxxxxxxxxxxx",
  "ownerEmail": "new@email.com",
  "vetoWindowHours": 168,
  "updatedAt": "2025-06-01T13:00:00.000Z"
}

Status codes

CodeMeaningWhen
200OKChange applied successfully
400Bad RequestInvalid or expired token
404Not FoundNo pending change found for this token
500Server ErrorInternal error

Example: Change owner email via curl

The following example shows the full workflow for changing an account's owner email address.

Step 1 — Request the change
curl -X POST https://ifgone.io/api/v1/account/change-request \
  -H "Content-Type: application/json" \
  -H "X-Account-Id: acc_xxxxxxxxxxxx" \
  -H "X-Signature: BASE64_DER_SIGNATURE" \
  -H "X-Timestamp: 1748764800" \
  -d '{"ownerEmail":"new@email.com"}'
Step 2 — Check pending change
curl https://ifgone.io/api/v1/account/pending-change \
  -H "X-Account-Id: acc_xxxxxxxxxxxx" \
  -H "X-Signature: BASE64_DER_SIGNATURE" \
  -H "X-Timestamp: 1748764860"
Step 3 — Approve (using token from email)
curl -X POST https://ifgone.io/api/v1/account/approve-change \
  -H "Content-Type: application/json" \
  -d '{"token":"TOKEN_FROM_EMAIL"}'
Cancel a pending change (optional)
curl -X DELETE https://ifgone.io/api/v1/account/pending-change \
  -H "X-Account-Id: acc_xxxxxxxxxxxx" \
  -H "X-Signature: BASE64_DER_SIGNATURE" \
  -H "X-Timestamp: 1748764920"

Updating a slot

Overwriting a slot replaces its encrypted contents entirely. Each update generates a new ephemeral ECDH key pair for that specific operation. The ephemeral key is used only for key agreement — it is not the private key, and it cannot be used to decrypt. The server stores only the encrypted payload; it never sees plaintext, never sees the private key, and cannot decrypt.

The private key is the only thing needed to decrypt (alongside the vault data). An executor with only the QR code (private key) can decrypt any slot — the ephemeral public key, salt, and IV are all stored in the vault.

  1. Encrypt your content using ECIES with a fresh ephemeral ECDH key pair and salt
  2. Overwrite the slot using POST /api/v1/slot/:slotId/update

Code examples

Complete examples for updating a slot in four languages. Each example shows encryption, signing, and the HTTP request.

Uses Node.js built-in crypto module. No additional dependencies.

⚠️ For demonstration purposes only — in production, never hardcode private keys or account IDs in your source code. This example uses a hardcoded BASE_URL, ACCOUNT_ID, and PRIVATE_KEY_PEM for simplicity. Load these from environment variables or a secrets manager instead. Error handling is also omitted for brevity.
import crypto from 'crypto';

const BASE_URL = 'https://ifgone.io';
const ACCOUNT_ID = 'acc_xxxxxxxxxxxx';
const PRIVATE_KEY_PEM = `[REDACTED PRIVATE KEY]`;

// --- 1. Sign a request (ECDSA-SHA256, P-256, DER-encoded) ---

function signRequest(body: string): { timestamp: string; signature: string } {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
  const message = `${timestamp}:${bodyHash}`;

  const signKey = crypto.createPrivateKey(PRIVATE_KEY_PEM);
  const sig = crypto.sign('sha256', Buffer.from(message), signKey);
  // Node.js crypto.sign() produces DER-encoded signatures natively

  return { timestamp, signature: sig.toString('base64') };
}

// --- 2. Encrypt slot data (ECIES: ECDH + HKDF-SHA256 + AES-256-GCM) ---

function encryptSlot(plaintext: string, publicKeyPem: string) {
  const recipientPub = crypto.createPublicKey(publicKeyPem);

  // Generate ephemeral ECDH key pair
  const { publicKey: ephPub, privateKey: ephPriv } =
    crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });

  // Derive shared secret via ECDH
  const sharedSecret = crypto.diffieHellman({
    publicKey: recipientPub,
    privateKey: ephPriv,
  });

  // Derive AES-256 key via HKDF-SHA256
  const salt = crypto.randomBytes(16);
  const aesKey = crypto.hkdfSync(
    'sha256', sharedSecret, salt,
    Buffer.from('ifgone-slot-encryption'), 32,
  );

  // Encrypt with AES-256-GCM
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv('aes-256-gcm', aesKey, iv);
  const ciphertext = Buffer.concat([
    cipher.update(plaintext, 'utf8'),
    cipher.final(),
  ]);
  const authTag = cipher.getAuthTag();

  // Export ephemeral public key as raw uncompressed point (65 bytes)
  const ephPubDer = ephPub.export({ type: 'spki', format: 'der' });
  const ephPubRaw = ephPubDer.slice(-65);

  return {
    encryptedData: Buffer.concat([ciphertext, authTag]).toString('base64'),
    encryptedDek: ephPubRaw.toString('base64'),
    iv: iv.toString('base64'),
    salt: salt.toString('base64'),
  };
}

// --- 3. Update a slot ---

async function updateSlot(slotId: number, content: string) {
  // Use the public key stored locally from account creation
  // (The public account lookup endpoint has been removed for security)
  const publicKey = LOCAL_PUBLIC_KEY;

  // Encrypt the content
  const encrypted = encryptSlot(content, publicKey);

  // Build and sign the request (accountId goes in header, not body)
  const body = JSON.stringify(encrypted);
  const { timestamp, signature } = signRequest(body);

  // Send the update
  const res = await fetch(`${BASE_URL}/api/v1/slot/${slotId}/update`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Account-Id': ACCOUNT_ID,
      'X-Signature': signature,
      'X-Timestamp': timestamp,
    },
    body,
  });

  return res.json();
}

// Overwrite slot 0 with new content
updateSlot(0, 'My updated secret data').then(console.log);

Local development build — data stored on this machine