Onsend Docs

Team, API Keys & Webhooks

Invite teammates with the right role, manage API keys for the public REST API, and subscribe your systems to claim webhook events.

These three settings pages live under Admin → Settings and govern who can operate your project and how outside systems integrate with it. Team controls human access, API keys manage machine credentials for the public REST API, and Webhooks push your project's claim events out to your own infrastructure.

Team

Open Settings → Team to see everyone who can access your project's admin surface, their role, and whether they've accepted yet.

Admin → Settings → Team: member list with role and accepted status
Admin → Settings → Team: member list with role and accepted status

Inviting a teammate

Enter the teammate's email, pick a role, and click Send invite.

Admin → Settings → Team → invite form (email + role)
Admin → Settings → Team → invite form (email + role)

The invitee needs an Onsend account first (V1)

In V1 you can only invite someone who already has an Onsend admin account. If their email has no account yet, the invite returns an error asking them to sign up at /sign-up first, then you retry. (A token-based invite-by-link flow, where the link itself creates the account, is on the roadmap.) Once invited, they appear as pending until the first time they sign in.

The invite emails the teammate a link to the admin dashboard. Email is best-effort: if the send fails, the membership is still created and the teammate can sign in to pick it up.

Roles

Every teammate holds exactly one role. Roles are ranked Owner > Admin > Member > Viewer, and you can only assign or modify roles strictly below your own.

RoleCapabilities
OwnerFull control of the project, including team and all settings. There is one Owner (the creator). Owner role can't be assigned, changed, or removed in the dashboard; ownership transfers go through Onsend support.
AdminManages the project end-to-end: campaigns, users, branding, and the Team, API keys and Webhooks pages. Can invite, re-role and remove Members and Viewers (but not other Admins or the Owner).
MemberDay-to-day operator: create and run campaigns, manage users and quests. No access to Team, API keys or Webhooks (those require Admin).
ViewerRead-only across the admin surface. Sees dashboards, campaigns and reports but can't change anything.

Who can manage the team

Inviting, re-roling and removing teammates all require Admin or higher. Members and Viewers can't open these actions. The Team list itself is visible to any role.

Changing a role or removing someone

In the member list, change a teammate's role with the inline role selector, or click Remove to revoke their access immediately. A few guardrails apply:

  • You can't change or remove yourself; a second Admin or the Owner has to do it. This prevents locking the project out of its last operator.
  • You can't touch the Owner; that's a support-only operation.
  • You can only act on teammates strictly below your own role, and you can't promote anyone to your own role or higher.

Every invite, role change and removal is written to the Audit Log.

API keys

The public REST API is live

Keys minted here authenticate Onsend's public REST API. Pass a key as Authorization: Bearer onsend_… (or X-API-Key); revocation and expiry take effect immediately. See the API Reference for endpoints, authentication, rate limits and payloads. (Per-scope enforcement is not in v1 yet — any valid key for your project can call every v1 endpoint.)

API keys are intended to let scripts, backend services and CI call Onsend on your project's behalf without a human session. Open Settings → API keys (requires Admin) to mint and manage them.

When you mint a key, Onsend shows the full key (formatted onsend_<…>) once, and stores only a SHA-256 hash plus a short prefix (never the raw key). Keys can be revoked from the list (a soft-delete retained for audit), and a revoked or expired key stops authenticating immediately. The exact endpoints, auth header, rate limits and scopes are documented in the API Reference.

Webhooks

Webhooks push your project's events to a URL you control, so your own backend can react in real time. Open Settings → Webhooks (requires Admin) to register endpoints.

Claim events today; more coming

Webhook delivery is live for claim lifecycle events (claim.confirmed / claim.failed), and every delivery is HMAC-signed so you can verify it came from Onsend (see Verifying the signature below). Other events you may have seen suggested, such as quest.completed Coming soon / referral.activated Coming soon, do not fire yet; subscribing to them delivers nothing.

Register an endpoint

Click New webhook, enter the destination URL and a comma-separated list of events to subscribe to.

Admin → Settings → Webhooks → New webhook (URL + events)
Admin → Settings → Webhooks → New webhook (URL + events)

Save the signing secret

On creation Onsend returns an HMAC signing secret (formatted whsec_<…>) once. Copy it now, because you'll use it to verify that incoming deliveries genuinely came from Onsend. You can pause (toggle active), edit, or delete a webhook later from the list.

Event types

Webhook delivery is live for claim lifecycle events only:

EventFires when
claim.confirmedA recipient's on-chain claim transaction is confirmed
claim.failedA claim transaction fails verification (after retries)

Claim webhooks need a per-campaign opt-in too (two gates)

Subscribing an endpoint here is necessary but not sufficient: you must also enable the event on the claim campaign itself:

  1. Subscribe an endpoint to the event here, in Settings → Webhooks.
  2. Enable it on the campaign: open it under Admin → Claim Campaigns, find the Webhook events card, tick claim.confirmed / claim.failed, and save.

Until both are set, your endpoint appears in the list but never delivers.

Coming soon: only claim events fire today

The events field accepts any string, but only the two claim events above are delivered today. quest.completed, referral.activated and others are recorded internally but do not fire webhooks yet, so don't rely on them.

Payload

Each delivery is a JSON POST. Claim events carry:

{
  "type": "claim.confirmed",
  "occurredAt": "2026-06-01T12:00:00.000Z",
  "receiptId": "rcpt_…",
  "tenantId": "…",
  "campaign": {
    "id": "…",
    "slug": "summer-airdrop",
    "name": "Summer Airdrop",
    "tokenSymbol": "ACME",
    "chain": "ETHEREUM"
  },
  "wallet": "0x…",
  "amount": "1000000000000000000",
  "txHash": "0x…",
  "status": "CONFIRMED"
}

amount is the raw on-chain token amount: a decimal string in base units, so divide by 10^decimals for the human value. type matches the x-vandergrid-event-type header.

Verifying the signature

Every delivery is a POST with a JSON body and these headers:

HeaderValue
x-vandergrid-signatureHex-encoded HMAC-SHA256 of the raw request body, keyed with your whsec_ secret
x-vandergrid-event-typeThe event type (e.g. claim.confirmed)

To verify, compute the HMAC-SHA256 of the raw request body with your secret and compare it (in constant time) to x-vandergrid-signature:

import { createHmac, timingSafeEqual } from "node:crypto";
 
function isValid(rawBody: string, signature: string, secret: string): boolean {
  const expected = createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
  if (expected.length !== signature.length) return false;
  return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(signature, "hex"));
}

Sign over the raw body

Compute the HMAC over the exact bytes Onsend sent: read the raw request body before any JSON parse + re-serialize. Re-serializing can reorder keys or change whitespace and break the comparison. Reject any request whose signature doesn't match.

Delivery & retries

A successful delivery is any 2xx response. Onsend does not follow redirects, so your endpoint must respond directly. Anything else (a non-2xx, a timeout, a connection error) is treated as a failure and retried with exponential backoff:

AttemptWait before this attempt
1immediate
21s
35s
430s
55min
630min
72hr
86hr

After 8 failed attempts the delivery is marked given up and no longer retried. Make your receiver idempotent, since a delivery can legitimately arrive more than once (e.g. when your endpoint 2xx's but the connection drops before Onsend records it).

Health at a glance

The webhook list shows each endpoint's last-delivered time and consecutive failure count, so you can spot a broken endpoint before deliveries are given up on.


This page covers the dashboard surfaces. The full per-endpoint REST reference, covering every route, request shape, scope and event payload, is documented separately: see the API Reference.

On this page