> ## Documentation Index
> Fetch the complete documentation index at: https://docs.grantex.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Offline Sync API

> API reference for syncing offline audit log entries to the Grantex cloud.

## 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

| Header | Value |
| - | - |
| `Authorization` | `Bearer <api_key>` |
| `Content-Type` | `application/json` |

### Request Body

| Field | Type | Required | Description |
| - | - | - | - |
| `bundleId` | `string` | Yes | The consent bundle ID that authorized these entries |
| `entries` | `SignedAuditEntry[]` | Yes | Array of signed, hash-chained audit entries |

### SignedAuditEntry Schema

| Field | Type | Description |
| - | - | - |
| `seq` | `number` | Monotonically increasing sequence number |
| `timestamp` | `string` | ISO-8601 timestamp of the action |
| `action` | `string` | Action identifier (e.g. `"calendar.read"`) |
| `agentDID` | `string` | DID of the agent that performed the action |
| `grantId` | `string` | Grant ID authorizing the action |
| `scopes` | `string[]` | Scopes on the grant at the time of the action |
| `result` | `string` | Outcome: `"success"`, `"auth_failure"`, `"scope_violation"`, `"execution_error"` |
| `metadata` | `object` | Optional arbitrary metadata |
| `prevHash` | `string` | SHA-256 hash of the previous entry (or `"0000000000000000"` for the first entry) |
| `hash` | `string` | SHA-256 hash of this entry |
| `signature` | `string` | Ed25519 signature of the `hash` field (hex-encoded) |

### Example Request

```bash theme={null}
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

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

### Response Fields

| Field | Type | Description |
| - | - | - |
| `accepted` | `number` | Number of entries successfully stored |
| `rejected` | `number` | Number of entries rejected |
| `revocationStatus` | `string` | Current grant revocation status: `"active"` or `"revoked"` |
| `revokedAt` | `string \| null` | ISO-8601 timestamp of revocation, or `null` if active |
| `errors` | `object[]` | Details of rejected entries |

### Error Array Schema

Each element in the `errors` array:

| Field | Type | Description |
| - | - | - |
| `seq` | `number` | Sequence number of the rejected entry |
| `code` | `string` | Error code |
| `message` | `string` | Human-readable error message |

### Error Codes for Rejected Entries

| Code | Description |
| - | - |
| `INVALID_HASH` | Recomputed hash does not match the entry's `hash` field |
| `INVALID_SIGNATURE` | Ed25519 signature does not verify against the bundle's public key |
| `BROKEN_CHAIN` | Entry's `prevHash` does not match the previous entry's `hash` |
| `DUPLICATE_SEQ` | Sequence number already exists (entry already synced) |
| `SEQ_GAP` | Non-consecutive sequence number (entries are missing) |

### Error Responses (HTTP Level)

| Status | Code | Description |
| - | - | - |
| 400 | `INVALID_REQUEST` | Missing or malformed request body |
| 401 | `UNAUTHORIZED` | Invalid or missing API key |
| 404 | `BUNDLE_NOT_FOUND` | The specified bundle does not exist |
| 413 | `PAYLOAD_TOO_LARGE` | Batch exceeds maximum size (1000 entries per request) |
| 429 | `RATE_LIMITED` | Too 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:**

| Scenario | Batch Size |
| - | - |
| Mobile device, metered connection | 50 |
| Raspberry Pi, Wi-Fi | 100 (default) |
| Server, reliable connection | 500 |

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

<CodeGroup>
  ```typescript TypeScript theme={null}
  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);
  }
  ```

  ```python Python theme={null}
  result = audit_log.sync(
      api_key="gx_...",
      bundle_id="cb_01HXYZ...",
  )

  print(f"Accepted: {result.accepted}")
  print(f"Rejected: {result.rejected}")
  print(f"Revocation: {result.revocation_status}")
  ```
</CodeGroup>

***

## Rate Limits

| Endpoint | Limit |
| - | - |
| `POST /v1/audit/offline-sync` | 20 requests/minute |

The rate limit applies per API key. If you need higher throughput, use larger batch sizes rather than more frequent requests.

## Ownership

Grantex is owned by Orchestrum Technologies LLP. Inventor and owner: Sanjeev Kumar. Ownership contact: [sanjeev@orchestrum.in](mailto:sanjeev@orchestrum.in) or [mishra.sanjeev@gmail.com](mailto:mishra.sanjeev@gmail.com).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.