REST API v1

GrailGuard Developer API

Integrate white-glove courier delivery into your platform. Get shipping quotes, create bookings, track packages, and receive real-time webhooks.

Authentication

All API requests require an API key. Include your key in the X-API-Key header or as an Authorization: Bearer token.

CURL
curl https://grailguard.io/api/v1/quote \
  -H "X-API-Key: gg_abc123def456-78901234abcdef5678"

API keys follow the format keyId-keySecret . The key ID is the first segment (before the hyphen); the secret is everything after. Keep your full key confidential.

Getting an API key: Contact your GrailGuard account manager or request one at grailguard.io/contact . Enterprise clients can generate keys via the admin panel.

Rate Limits

Each API key has a per-minute rate limit (default: 60 requests/min). When exceeded, the API returns 429 Too Many Requests . The response includes a Retry-After header with the number of seconds to wait.

RESPONSE
{
  "error": "Rate limit exceeded. Try again in 42s"
}

Error Handling

The API uses standard HTTP status codes. All error responses include a JSON body with an error field.

Code Meaning
400 Bad Request — missing or invalid parameters
401 Unauthorized — missing or invalid API key
403 Forbidden — key lacks the required scope
404 Not Found — resource does not exist
429 Rate limit exceeded
500 Internal server error

Endpoints

Get a shipping quote based on origin, destination, and item value. Quotes are valid for 24 hours.

Query Parameters

Param Type Description
pickup_zip required string Origin ZIP / postal code
delivery_zip required string Destination ZIP / postal code
declared_value number Item value in USD (default: 5000)
service_tier string metro , standard , or international (default: standard)

Example

CURL
curl "https://grailguard.io/api/v1/quote?pickup_zip=10001&delivery_zip=90210&declared_value=25000&service_tier=standard" \
  -H "X-API-Key: YOUR_API_KEY"
RESPONSE
{
  "quote": {
    "baseFee": 400,
    "insurancePremium": 375,
    "total": 775,
    "currency": "USD",
    "serviceTier": "standard",
    "declaredValue": 25000,
    "estimatedTransit": "1-3 business days",
    "validFor": "24 hours"
  }
}

Create a new delivery booking. Returns a tracking number and tracking URL.

Request Body (JSON)

Field Type Description
customer_name required string Full name of the sender
customer_email required string Email for delivery notifications
customer_phone string Phone number
pickup_address required string Full pickup address
delivery_address required string Full delivery address
item_description string Description of the item
declared_value number Item value in USD for insurance
service_tier string metro , standard , or international
stripe_payment_id string Stripe PaymentIntent ID if pre-paid

Example

CURL
curl -X POST https://grailguard.io/api/v1/bookings \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_name": "Jane Doe",
    "customer_email": "jane@example.com",
    "pickup_address": "123 Main St, New York, NY 10001",
    "delivery_address": "456 Oak Ave, Los Angeles, CA 90210",
    "item_description": "Patek Philippe Nautilus 5711",
    "declared_value": 125000,
    "service_tier": "standard"
  }'
RESPONSE 201
{
  "booking": {
    "id": 42,
    "trackingNumber": "GG-2026-A7X9K3",
    "status": "confirmed",
    "trackingUrl": "https://grailguard.io/track.html?tn=GG-2026-A7X9K3",
    "createdAt": "2026-04-15T14:30:00.000Z"
  }
}

Retrieve full booking details including all tracking events.

Example

CURL
curl https://grailguard.io/api/v1/bookings/GG-2026-A7X9K3 \
  -H "X-API-Key: YOUR_API_KEY"
RESPONSE
{
  "booking": {
    "trackingNumber": "GG-2026-A7X9K3",
    "status": "in_transit",
    "serviceTier": "standard",
    "customer": { "name": "Jane Doe", "email": "jane@example.com" },
    "pickup": "123 Main St, New York, NY 10001",
    "delivery": "456 Oak Ave, Los Angeles, CA 90210",
    "item": { "description": "Patek Philippe Nautilus 5711", "declaredValue": 125000 },
    "courier": "Alex Rivera",
    "payment": { "status": "paid", "amount": 1900 },
    "scheduledDate": "2026-04-16",
    "createdAt": "2026-04-15T14:30:00.000Z",
    "updatedAt": "2026-04-15T18:00:00.000Z",
    "trackingUrl": "https://grailguard.io/track.html?tn=GG-2026-A7X9K3"
  },
  "events": [
    { "status": "Booking Confirmed", "location": "New York, NY", "description": "Booking created", "timestamp": "2026-04-15T14:30:00.000Z" },
    { "status": "Courier Assigned", "location": "New York, NY", "description": "Courier Alex Rivera assigned", "timestamp": "2026-04-15T15:00:00.000Z" },
    { "status": "Picked Up", "location": "123 Main St, New York, NY", "description": "Item collected", "timestamp": "2026-04-16T09:00:00.000Z" }
  ]
}

List all bookings with pagination and optional status filtering.

Query Parameters

Param Type Description
page int Page number (default: 1)
limit int Results per page, max 100 (default: 25)
status string Filter by status (e.g. confirmed , in_transit , delivered )

Example

CURL
curl "https://grailguard.io/api/v1/bookings?page=1&limit=10&status=confirmed" \
  -H "X-API-Key: YOUR_API_KEY"
RESPONSE
{
  "bookings": [
    {
      "trackingNumber": "GG-2026-A7X9K3",
      "customer": "Jane Doe",
      "status": "confirmed",
      "tier": "standard",
      "amount": 775,
      "createdAt": "2026-04-15T14:30:00.000Z",
      "updatedAt": "2026-04-15T14:30:00.000Z"
    }
  ],
  "total": 1,
  "page": 1,
  "pages": 1
}

Webhooks

Subscribe to real-time events so your system is notified when booking statuses change.

Register a new webhook subscription. Returns a signing secret for payload verification.

Request Body (JSON)

Field Type Description
url required string HTTPS endpoint to receive events
events string Comma-separated event types (default: booking.status_changed )
Available events: booking.created , booking.status_changed , booking.delivered , booking.cancelled

Example

CURL
curl -X POST https://grailguard.io/api/v1/webhooks \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://your-app.com/webhooks/grailguard", "events": "booking.status_changed,booking.delivered" }'
RESPONSE 201
{
  "webhook": {
    "id": 7,
    "url": "https://your-app.com/webhooks/grailguard",
    "events": ["booking.status_changed", "booking.delivered"],
    "secret": "a1b2c3d4e5f6...",
    "note": "Save this secret - we use it to sign webhook payloads via HMAC-SHA256 in the X-GrailGuard-Signature header."
  }
}

List all active webhook subscriptions for your API key.

Remove a webhook subscription by ID.

Scopes & Permissions

API keys are assigned scopes that control which endpoints they can access. Scopes are cumulative — a key with admin scope also has write and read access.

Scope Endpoints Use Case
read GET /quote, GET /bookings, GET /bookings/:tn Tracking integrations, dashboards
write POST /bookings + all read endpoints Booking creation (e-commerce, platforms)
admin All endpoints including webhooks Full integration partners

Verifying Webhook Signatures

Every webhook POST includes an X-GrailGuard-Signature header containing an HMAC-SHA256 hash of the JSON payload, signed with the secret returned when you created the subscription.

NODE.JS
const crypto = require('crypto');

function verifyWebhook(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// Express handler
app.post('/webhooks/grailguard', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-grailguard-signature'];
  if (!verifyWebhook(req.body, sig, process.env.GG_WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  const event = JSON.parse(req.body);
  console.log('Event:', event.type, event.data);
  res.sendStatus(200);
});
Security: Always verify signatures before processing webhook events. Reject any request where the signature does not match. Use crypto.timingSafeEqual to prevent timing attacks.

Webhook Payload Format

JSON
{
  "type": "booking.status_changed",
  "timestamp": "2026-04-15T18:00:00.000Z",
  "data": {
    "trackingNumber": "GG-2026-A7X9K3",
    "previousStatus": "confirmed",
    "newStatus": "in_transit",
    "courier": "Alex Rivera",
    "updatedAt": "2026-04-15T18:00:00.000Z"
  }
}