Webhooks
Blank delivers events to your HTTPS endpoint as signed CloudEvents 1.0 JSON. Webhooks replace polling for anything you need to react to promptly. Webhooks need an API key. Creating, updating, deleting, rotating and replaying require a key withwebhooks:write; reading endpoints and deliveries requires webhooks:read. This holds even when every read you care about is anonymous, such as staking or token data.
Limits
Deleted endpoints do not count toward the endpoint limit. The per-batch figure is a fair-share bound rather than a quota: dispatch takes at most that many of your deliveries per pass so one account’s backlog cannot delay everyone else’s, and your remaining deliveries are picked up on subsequent passes in the usual order.
Operations
All mutations require an
Idempotency-Key. Update, delete, and rotate additionally require If-Match with the endpoint’s current version.
Event types
An endpoint can subscribe to a reserved type, and the subscription is accepted, but Blank sends nothing for it until the type ships. Until then, poll the matching read:
GET /presales/{id} for presales. The API has no audience export yet.
prediction.submitted and operation.failed go only to endpoints owned by the account that submitted the prediction or owns the operation. Every other delivered type is a public market event: every active endpoint subscribed to the type receives it, for every visible token. Hidden tokens send nothing.
Types are versioned with a .v1 suffix. A breaking payload change ships as a new type, never as a mutation of an existing one.
Create an endpoint
URL policy
Endpoint URLs are validated at creation and again on every delivery attempt. A URL is rejected with422 webhook_url_disallowed unless it:
- uses
https:on the default port (no port, or443) - carries no userinfo and no fragment
- resolves to a public, fully qualified hostname — not
localhost, and not a hostname ending in.localhost,.local,.internal,.home.arpa, or.onion - is not a private, loopback, link-local, carrier-grade NAT, documentation, or multicast IP address, in IPv4 or IPv6
The delivery request
Blank sendsPOST with these headers:
The body is a CloudEvent:
data carries the allowlisted payload for that event type. Treat unknown fields as additive and ignore them.
Event payloads
build.blank.trade.confirmed.v1
Sent once for each trade Blank indexes on a visible token. subject is token_trade/{id}, and data is the same Trade object that GET /tokens/{mint}/trades returns. A trade the indexer sees again, for example after a retry, sends nothing.
build.blank.staking.position.updated.v1
Sent when Blank indexes a new staking position, a change to one, or its removal. subject is staking_position/{id}. data.change says which:
data.position is the StakingPosition object that blank.staking.walletPositions() returns, as of the change, without the reward fields (pendingRewardsLamports, totalRewardsClaimedLamports, claimable, rewardsAsOf). Rewards are read live from chain, so call walletPositions() when you need them. A closed position carries its last indexed values with amountRaw and weightedAmountRaw set to "0", and updatedAt set to the time it was removed. Re-indexing a position without a change sends nothing.
build.blank.launch.created.v1 and build.blank.launch.confirmed.v1
subject is token_launch/{id}, and data is the Token object that GET /tokens/{mint} returns at the moment of the change. Each is sent at most once per token.
A launch that is still waiting for its creator’s signature sends nothing: it may never reach the chain. A token that is hidden when it launches sends neither event.
build.blank.fee.distribution.created.v1
Sent once for each fee distribution Blank records on a visible token. subject is fee_distribution/{id}, and data is the item that GET /tokens/{mint}/fee-distributions returns, plus the token’s mintAddress, since the item alone does not name the token. A distribution the indexer sees again sends nothing.
build.blank.prediction.round.locked.v1 and build.blank.prediction.round.settled.v1
subject is prediction_round/{id}, and data is the PredictionRound object that GET /tokens/{mint}/prediction-rounds/{roundId} returns. round.locked is sent once when the round stops taking predictions, and round.settled once when its settlement price and ranks are final. A settlement that fails and is retried does not send a second round.locked. predictionCount in round.locked counts the predictions recorded when the round locked.
build.blank.operation.failed.v1
Sent once when one of your asynchronous operations fails, and only to endpoints owned by the account that owns the operation. subject is api_operation/{id}, and data is the Operation object that GET /operations/{operationId} returns, with the Problem Details of the failure in error.
data.position.updatedAt, trades by data.blockTime, and fee distributions by data.collectedAt, and deduplicate on Blank-Event-Id. When you need the latest state rather than the history, read the resource from the API.
Verifying the signature
Blank-Signature is an HMAC-SHA-256 over "{timestamp}.{raw request body}", hex-encoded. Verify the exact raw bytes before parsing JSON — re-serializing the body changes the signature.
verifyWebhookSignature enforces a timestamp tolerance of 300 seconds by default, rejecting replays outside that window. Override it with toleranceSeconds (1 to 3,600) only if you have a reason to. It checks every v1 signature against secret, and every v0 signature against previousSecret when you supply one.
Responding
- Return any
2xxonly after the event is durably accepted. Write it to a queue or a table first, then acknowledge. - Anything other than
2xx, a timeout, or a transport failure counts as a failed attempt and is retried. - The delivery timeout is 10 seconds. Do the work asynchronously.
- Deduplicate on
Blank-Event-Id. Retries and replays reuse the same event ID with a new delivery ID, and at-least-once delivery is the contract.
Retries and dead letters
Each delivery gets up to 7 attempts: the first, then six retries at roughly 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours, and 24 hours. Each delay is jittered, and aRetry-After header on your response is honoured up to 24 hours.
Delivery status moves through:
Inspecting deliveries
id, endpointId, eventId, eventType, status, attemptCount, nextAttemptAt, responseStatus, lastErrorCode, deliveredAt, createdAt, updatedAt. Filter by status and paginate with cursor and limit.
lastErrorCode distinguishes transport failures (request_timeout, network_error, url_disallowed, redirect_limit_exceeded, redirect_location_missing) from HTTP rejections, which are recorded as http_<status>. The complete stable list is in Errors.
Replaying
Blank-Delivery-Id and the original Blank-Event-Id. Use it to drain dead letters after fixing your endpoint. A delivery that is not in a replayable state returns 409 webhook_delivery_not_replayable; an unknown or foreign delivery returns 404 webhook_delivery_not_found.
Rotating the signing secret
Rotation issues a new secret and keeps the previous one valid for a bounded overlap window, so you can deploy without dropping in-flight deliveries.overlapHours is an integer from 1 to 24, defaulting to 24. During the overlap, Blank signs each delivery with both secrets: v1 with the new secret and v0 with the previous one.
Safe rotation:
- Rotate and store the new secret as
BLANK_WEBHOOK_SECRET. - Move the old value to
BLANK_PREVIOUS_WEBHOOK_SECRETand deploy, so your handler accepts both. - Once the overlap window closes, remove
BLANK_PREVIOUS_WEBHOOK_SECRET.
Updating and deleting
Both use optimistic concurrency:update accepts any non-empty subset of name, url, eventTypes, and status (active or disabled). A stale version returns 412 precondition_failed — re-read the endpoint and retry.
delete soft-deletes the endpoint, stops all future fanout, and returns { id, deletedAt }.
Errors
Shared errors apply too. See Errors and Conventions.