Skip to content

Webhooks for apps beta ​

The Console tells an app acting for its users what happens in its teams - an order went through, a product is ready - so it does not have to poll.

  • Webhook endpoints are set up by Anikeen on request: your app's client id, a display name, the URL and the event types. You get the endpoint's signing secret whsec_… once (it can be rolled).
  • Only teams managed by an app produce events, and only that app's active endpoints that listen to the type receive them.

Request format ​

The same format as Anikeen ID's webhooks, so you verify both with the same code:

http
POST <endpoint url>
Content-Type: application/json
User-Agent: Anikeen-Console-Webhooks/1.0
Anikeen-Event-Id: 01JF8Z…
Anikeen-Event-Type: product.ready
Anikeen-Signature: t=1731150000,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with the secret>

{"id": "01JF8Z…", "type": "product.ready", "created": 1731150000,
 "data": {"object": {"id": "01JF…", "family": "container", "team_id": "01JE…", "status": "running", …}}}
Part
idthe event id (also in Anikeen-Event-Id)
typethe event type (also in Anikeen-Event-Type)
createdUnix timestamp of the event
data.objectthe object - see Events
data.reasonfor product.updated and product.deleted

The envelope is frozen when the event happens; every attempt sends the same body, signed anew (the signature's timestamp is the attempt's).

Verifying the signature ​

Anikeen-Signature is t=<unix time>,v1=<signature>, where the signature is the hex HMAC-SHA256 of "<t>.<raw body>" with your endpoint's secret. Always verify against the raw request body, and reject old timestamps (anikeen/id allows 5 minutes by default).

php
use Anikeen\Id\Exceptions\SignatureVerificationException;
use Anikeen\Id\Webhooks\Webhook;

try {
    $event = Webhook::constructEvent(
        $request->getContent(),
        $request->header('Anikeen-Signature'),
        $secret,
    );
} catch (SignatureVerificationException) {
    abort(400);
}
// ['id' => …, 'type' => 'product.ready', 'created' => …, 'data' => ['object' => […]]]
php
// routes/web.php - verifies, de-duplicates and dispatches Laravel events
Route::anikeenConsoleWebhooks('webhooks/anikeen-console');
js
import crypto from 'node:crypto'

function verify(rawBody, header, secret, tolerance = 300) {
  let t = null
  const signatures = []
  for (const part of (header ?? '').split(',')) {
    const [key, value] = part.trim().split('=', 2)
    if (key === 't' && /^\d+$/.test(value ?? '')) t = value
    else if (key === 'v1' && value) signatures.push(value)
  }
  if (t === null || signatures.length === 0) return false
  const expected = Buffer.from(
    crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'),
  )
  const matches = signatures.some((s) => {
    const given = Buffer.from(s)
    return given.length === expected.length && crypto.timingSafeEqual(expected, given)
  })
  return matches && Math.abs(Date.now() / 1000 - Number(t)) <= tolerance
}

See SDK webhooks for the Laravel route and events.

Events ​

An endpoint lists the types it wants, or * for all; webhook.test is always sent.

TypeWhendata.object
order.completedthe order was confirmed in Anikeen ID, provisioning startedthe order as GET /teams/{team}/orders/{order} (checkout_url always null)
order.failedthe order was cancelled or expired (or its checkout could not be opened)the order (status cancelled, expired or failed)
product.readya product was built (a container: created and started)the product
product.failedbuilding an item failed (revoked and refunded, as in the Console){id: null, family, team_id, status: "failed", order_id, order_item_id, product_id, error}
product.updatedsuspended / resumed for billing, rescaled, reinstalledthe product; data.reason: billing_suspended, billing_resumed, rescaled, reinstalled
product.deleteddeleted (by the app, the contract ended in Anikeen ID, …)the product (status: "deleted"); data.reason: deleted or contract_ended
team.releasedthe app lost access to the team (see Ending it); the last event for the team{id, name, app: {id, name}, released_at}
webhook.testa test event sent by Anikeen{message}

The product object ​

{id, family, team_id, status, …} plus everything the family's GET /<family>/{id} returns (e.g. a container's location, template, gpu, addresses).

status is the family's live status where it has one and it was read (a container's or server's running, stopped, … on product.ready, rescale and reinstall), otherwise active, provisioning (still being set up beta), suspended (billing) or deleted. On product.ready the product is set up: its provisioning_status is null (see Products being set up).

Read the product from the API when you need its current state: the API is the source of truth.

Delivery and retries ​

  • A 2xx answer within 10 seconds counts as delivered. Redirects are not followed.
  • Otherwise the attempt is retried after 1 min, 5 min, 30 min, 2 h, 5 h, 10 h and 3 × 12 h (10 attempts over about two days), then the delivery has failed.
  • Events are delivered at least once and not necessarily in order: de-duplicate on the event id (Anikeen-Event-Id) and answer 2xx for an id you have already handled.
  • Endpoint URLs must use https and resolve to public addresses only (checked when saved and before every attempt).
  • Events and deliveries are kept for 30 days; Anikeen can see each delivery's attempts and your endpoint's answers, and resend it.

Anikeen Console documentation