Skip to main content

Overview

The offline sync endpoint receives audit log entries produced by on-device agents operating with consent bundles. The server verifies the Ed25519 signatures and hash chain integrity, then reconciles entries with the cloud audit log.

Sync Offline Audit Entries

Upload a batch of offline audit log entries to the Grantex cloud.
POST /v1/audit/offline-sync

Request Headers

HeaderValue
AuthorizationBearer <api_key>
Content-Typeapplication/json

Request Body

FieldTypeRequiredDescription
bundleIdstringYesThe consent bundle ID that authorized these entries
entriesSignedAuditEntry[]YesArray of signed, hash-chained audit entries

SignedAuditEntry Schema

FieldTypeDescription
seqnumberMonotonically increasing sequence number
timestampstringISO-8601 timestamp of the action
actionstringAction identifier (e.g. "calendar.read")
agentDIDstringDID of the agent that performed the action
grantIdstringGrant ID authorizing the action
scopesstring[]Scopes on the grant at the time of the action
resultstringOutcome: "success", "auth_failure", "scope_violation", "execution_error"
metadataobjectOptional arbitrary metadata
prevHashstringSHA-256 hash of the previous entry (or "0000000000000000" for the first entry)
hashstringSHA-256 hash of this entry
signaturestringEd25519 signature of the hash field (hex-encoded)

Example Request

curl -X POST https://api.grantex.dev/v1/audit/offline-sync \
  -H "Authorization: Bearer gx_..." \
  -H "Content-Type: application/json" \
  -d '{
    "bundleId": "cb_01HXYZ...",
    "entries": [
      {
        "seq": 1,
        "timestamp": "2026-04-03T12:00:00.000Z",
        "action": "calendar.read",
        "agentDID": "did:grantex:ag_01HXYZ...",
        "grantId": "grnt_01HXYZ...",
        "scopes": ["calendar:read"],
        "result": "success",
        "metadata": {"eventCount": 12},
        "prevHash": "0000000000000000",
        "hash": "a3f29c81b4e2f1a7d8c3e5b6a9f0d1c2e4b7a8c9d0e1f2a3b4c5d6e7f8a9b0c1",
        "signature": "4d5e6f7a8b9c0d1e..."
      }
    ]
  }'

Response — 200 OK

{
  "accepted": 1,
  "rejected": 0,
  "revocationStatus": "active",
  "revokedAt": null,
  "errors": []
}

Response Fields

FieldTypeDescription
acceptednumberNumber of entries successfully stored
rejectednumberNumber of entries rejected
revocationStatusstringCurrent grant revocation status: "active" or "revoked"
revokedAtstring | nullISO-8601 timestamp of revocation, or null if active
errorsobject[]Details of rejected entries

Error Array Schema

Each element in the errors array:
FieldTypeDescription
seqnumberSequence number of the rejected entry
codestringError code
messagestringHuman-readable error message

Error Codes for Rejected Entries

CodeDescription
INVALID_HASHRecomputed hash does not match the entry’s hash field
INVALID_SIGNATUREEd25519 signature does not verify against the bundle’s public key
BROKEN_CHAINEntry’s prevHash does not match the previous entry’s hash
DUPLICATE_SEQSequence number already exists (entry already synced)
SEQ_GAPNon-consecutive sequence number (entries are missing)

Error Responses (HTTP Level)

StatusCodeDescription
400INVALID_REQUESTMissing or malformed request body
401UNAUTHORIZEDInvalid or missing API key
404BUNDLE_NOT_FOUNDThe specified bundle does not exist
413PAYLOAD_TOO_LARGEBatch exceeds maximum size (1000 entries per request)
429RATE_LIMITEDToo many requests

Batch Processing

The endpoint processes entries in order. If an entry fails validation, subsequent entries in the same batch may still be accepted if their hash chain is internally consistent. Recommended batch sizes:
ScenarioBatch Size
Mobile device, metered connection50
Raspberry Pi, Wi-Fi100 (default)
Server, reliable connection500
Maximum entries per request: 1000.

Sync Flow

The recommended sync flow:
Device                                      Grantex API
  │                                             │
  │── POST /v1/audit/offline-sync ─────────────►│
  │   { bundleId, entries[1..100] }              │
  │                                             │
  │◄── 200 { accepted: 98, rejected: 2, ... } ──│
  │                                             │
  │   (check revocationStatus)                  │
  │   if "revoked" → stop, delete bundle        │
  │   if "active" → mark entries synced          │
  │                                             │
  │── POST /v1/audit/offline-sync ─────────────►│
  │   { bundleId, entries[101..150] }            │
  │                                             │
  │◄── 200 { accepted: 50, rejected: 0, ... } ──│
  │                                             │
  │   (all entries synced)                       │
  │                                             │

Idempotency

Syncing the same entries multiple times is safe. The server deduplicates by (bundleId, seq) pair. If an entry with the same sequence number already exists and has a matching hash, it is silently accepted. If the hash differs, it is rejected with DUPLICATE_SEQ.

SDK Examples

import { syncAuditLog } from '@grantex/gemma';

const result = await syncAuditLog(auditLog, {
  endpoint: 'https://api.grantex.dev',
  apiKey: 'gx_...',
  bundleId: 'cb_01HXYZ...',
  batchSize: 100,
});

console.log(`Synced: ${result.syncedCount}`);
if (result.hasErrors) {
  console.warn('Errors:', result.errors);
}

Rate Limits

EndpointLimit
POST /v1/audit/offline-sync20 requests/minute
The rate limit applies per API key. If you need higher throughput, use larger batch sizes rather than more frequent requests.