Skip to content
Coritan Docs

Receive organization webhooks

Send your organization's events to an endpoint on your own server, and learn how deliveries, retries and signatures work.

View as Markdown

A webhook tells your own systems when something happens in your organization: a customer signs up, a customer orders a service, a service starts or ends, an invoice is paid, a deployment's release goes live. For each event you subscribe to, we send an HTTPS POST with a JSON body to the endpoint you choose, signed with the webhook's own secret.

  • You need the owner or admin role in the organization. The Webhooks page is not in the menu for other roles.
  • Set up an HTTPS endpoint that answers with a 2xx status within 10 seconds, at a public address of your own rather than one of Coritan's, or on one of your organization's own deployments on App Deployment (Send events to your own deployment).
  • Have a safe place for the endpoint to keep the webhook's signing secret. We show the secret once, when you create the webhook and each time you rotate its secret (Signatures).
  1. Go to Organizations, open your organization, then choose Settings, then Webhooks. In the sidebar, you find them in your organization's block, under Settings.
  2. Choose New webhook….
  3. Enter the Endpoint URL. It must start with https://, and cannot be a private address or one of Coritan's own, such as a brand's site or the API. Your organization's own deployments are the exception (Send events to your own deployment).
  4. Tick the Events to send, and under Deployment events the events of the organization's deployments (Deployment events). To subscribe to an event the list does not offer, such as commerce.order.placed, type its name in Other events; separate several names with commas.
  5. Choose Create webhook.
  6. The dialog says Webhook created and Copy the signing secret now. Select Copy secret under Signing secret, store the secret where your endpoint can read it, then choose Done. It is not shown again.

Change, turn off or delete a webhook

Section titled Change, turn off or delete a webhook

Each row of the list has a menu at its end, and a webhook's own page has the same actions in its header.

  • To change the address or the events, choose Edit…, make the change in Edit webhook and choose Save changes.
  • To stop deliveries and keep the settings, use the switch in the list's Enabled column, choose Turn off in the menu on the webhook's page, or untick Enabled in Edit webhook. A webhook that is off shows Turn on on its page. Events that happen while it is off are not sent later.
  • To delete a webhook, choose Delete webhook…, type the endpoint's host name and choose Delete webhook. Deliveries stop at once, and its delivery history is deleted with it.

Rotate the secret when it may have leaked, or when you have lost it. Choose Rotate secret… in the webhook's menu, on its row or on its page, then Rotate secret. The dialog New signing secret shows the new secret once; copy it as you did the first. The old secret stops working at once, and every delivery from then on is signed with the new one, retries of earlier events included, so give your endpoint the new secret straight away.

Send events to your own deployment

Section titled Send events to your own deployment

An app your organization runs on App Deployment can receive the organization's webhooks itself, so it does not have to poll. Use one of these as the Endpoint URL, on the default https port:

  • the deployment's platform address, such as https://web-shop.apps.example.net/<path>, shown in the deployment's header;
  • a custom domain of the deployment, once the deployment's Domains tab shows it as verified;
  • the domain of a web proxy in front of a container or server your organization runs on Coritan, once the proxy is linked to that service. Ask Coritan support to link it.

The deployment or service must belong to this organization and be active. We send the delivery through Coritan's public edge, as a visitor's request would arrive: the name must lead to the edge, and never to a private address. A deployment of another organization is refused like any other Coritan address.

We check again before each delivery. When the deployment or service has moved to another organization, stopped, or its domain is no longer verified, nothing is sent and the attempt counts as a failure.

  1. On the Webhooks page, select the webhook's row. Its page opens.
  2. Read the Endpoint card: the Failures in a row, when we Last delivered to it, when it was Created, and the events it is sent. When deliveries keep failing or the webhook is off, a note above the card says so.
  3. Read the Deliveries card. It lists each event sent to the webhook, newest first, 25 to a page, with its Event, its Status, the HTTP status the endpoint gave as its Answer (or No answer), the Attempts made and When the event happened. To see one status only, choose it in the list above the table, which also counts each one.
  4. Select a delivery to read its details: its Status, Answer and Attempts, when it was Created and Delivered, when the Next attempt is due while it waits, its Delivery ID, and the headers of the Request.

The dashboard does not show the body we sent or what the endpoint answered. A delivery has one of these statuses:

Status What it means
Waiting Not sent yet, or waiting to be tried again after a failure.
Sending An attempt is on its way.
Delivered The endpoint answered with a 2xx status.
Failed Every attempt failed (Deliveries and retries).
Cancelled The webhook was off when an attempt fell due, so we did not send it.

The webhook appears in the list with its events, whether it is Enabled, its Failures in a row and when we Last delivered to it. Its page lists every delivery and how the endpoint answered.

The event name arrives in the X-Webhook-Event header. These are the events we send today, with the service events and the deployment events below:

Event When Body fields
customer.created A customer registers or signs in with a social account for the first time, you add one, or an account moves from your old site as a new account here. customer_id, email
service.ordered A member orders a service for a customer from the dashboard or the API. service_id, customer_id, product_id
order.created A customer orders from your storefront. service_id, customer_id, product_id, invoice_id
invoice.created Our daily billing run creates invoices for your customers. Sent in the platform_mor and hybrid billing modes. count, generated_at
invoice.paid An invoice is paid in full. invoice_id, invoice_number, amount, customer_id, gateway
payout.created We create a payout for your organization. payout_id, period_start, period_end

Subscribe to * to receive every event. The Commerce API sends its own events with names that start with commerce., such as commerce.order.placed; Sell with the Commerce API lists them.

An earlier version of the dashboard's list offered customer.updated, service.created, invoice.overdue and payout.completed, which we do not send. A webhook that names them shows them in Other events when you choose Edit…. Remove them there.

Seven events follow each customer service from its first payment to its end. They tell your own systems when to give a customer what they bought and when to take it back: a rank or a licence you sell as a Standalone product, or a server Coritan runs for a Platform product.

service.activated
A new service starts: its first invoice is paid, or Coritan finishes setting up a server whose order had not started yet.
service.renewed
A renewal invoice is paid, and next_due_date moves to the end of the new period.
service.suspended
The service stops: a renewal was not paid in time, the customer's card issuer warned that a payment may be disputed, it was suspended for abuse, or a member or Coritan suspended it.
service.unsuspended
A suspended service runs again: the unpaid invoice is paid, Coritan finds the card issuer's warning was wrong, or a member or Coritan lifts the suspension.
service.cancelled
The service is set to end and has not ended yet, and termination_date says when. Cancelled at the end of its term, it keeps running until then. Given a deletion date after an abuse suspension, it stays suspended until then. A server cancelled at once shows pending_termination while Coritan removes it.
service.cancellation_withdrawn
A customer took back a cancellation set for the end of the term (Take a cancellation back). termination_date is null again, status is still active, and the service renews as usual.
service.terminated
The service has ended: it was cancelled at once, refunded or deleted for abuse, Coritan ended it, or its termination_date passed. A Standalone service ends at our hourly check after that date.

Repeating a change sends nothing: lifting the suspension of a service that is already active, or cancelling it again for the same date.

The body describes the service as it is right after the change:

event
The event's name, as in the X-Webhook-Event header.
event_id
The event's own ID. It stays the same across every delivery of the event (Deliveries and retries).
occurred_at
When the change happened, in UTC.
service_id, customer_id, product_id
The service, the customer it belongs to, and the product it was ordered from.
product_slug, product_type
The product's slug, and its kind: custom_standalone for Standalone or platform_linked for a Platform product.
status, previous_status
The service's status after the change and before it. status is active, suspended, pending_termination or terminated.
billing_cycle
The plan's billing cycle, such as monthly. A customer can switch a Standalone service to another billing period at a renewal (Switch a rank between monthly and annual billing), so the service.renewed event after that carries the new one.
next_due_date
When the next renewal invoice falls due, in UTC, or null once the service has ended.
termination_date
When the service ends or ended, in UTC, or null when it is not set to end.
reason
Why the service changed, from the table below.
config
The service's settings: what the customer gave when ordering, such as the answers to the product's order form, and settings Coritan keeps on the service, such as termination_date. Keys that start with _ are left out.
JSON
{
  "event": "service.activated",
  "event_id": "5f0c9e1a7b2d4c8e9a3b6d1f0e2c4a7b",
  "occurred_at": "2026-10-08T14:02:11.204118+00:00",
  "service_id": 812,
  "customer_id": 2291,
  "product_id": 14,
  "product_slug": "vip-rank",
  "product_type": "custom_standalone",
  "status": "active",
  "previous_status": "pending",
  "billing_cycle": "monthly",
  "next_due_date": "2026-11-08T14:02:11.198305+00:00",
  "termination_date": null,
  "reason": "payment",
  "config": {"minecraft_name": "Steve"}
}
reason What changed the service
payment An invoice for the service was paid.
unpaid A renewal invoice was not paid in time.
dispute The customer's card issuer warned that a payment may be disputed.
dispute_cleared Coritan found that warning was wrong and put the customer's services back.
abuse Your staff or Coritan acted on abuse.
staff A member of your organization, in the dashboard, the staff console or the API.
customer The customer, from their account.
refund The invoice that paid for the service was refunded.
term_ended The service's termination_date passed.
provisioned Coritan finished setting up the service.
platform Coritan staff, or Coritan's own systems, such as when they stop or remove the server behind the service.

The organization's own deployments on App Deployment send these events. Subscribe to deployment.* for all of them.

Event When
deployment.created A deployment is made, once the order that made it is set up.
deployment.deleted A deployment is deleted: from the dashboard or the API, by Coritan, or because the service that paid for it ended.
deployment.release.ready A release is ready and serving: as the live release in production, as a preview, or as a canary beside the live release.
deployment.release.failed A release fails.
deployment.instance.unhealthy An instance fails its health checks.
deployment.spend.alert The deployment's own spend limit reaches 80% or 100%, and 50% on pay as you go. The limit for every deployment on the account sends none.
deployment.alert.error_spike The share of server errors in an hour spikes.
deployment.alert.wake_storm Visitors keep waking a deployment that sleeps when idle.
deployment.instance.restarted An instance restarted and we did not cause it, such as after it ran out of memory.
deployment.instance.replaced We replaced an instance with a fresh one, because its memory stayed close to its limit or because it still ran an earlier memory limit.
deployment.alert.out_of_memory An instance restarted after using 90% or more of its memory limit, or restarted twice or more in an hour.
deployment.memory.limit_changed The memory limit of a web service or a hybrid site moved one step, up or down (Memory grows automatically).
deployment.alert.memory_at_limit An instance uses 90% or more of its memory limit and the limit cannot grow.

Their body names the event in type, the deployment in deployment (its uuid, name and kind) and the details in data, which never carries a password, a secret or a key. id is the same for every delivery of one event.

The instance events and the memory events carry these details in data:

  • deployment.instance.restarted has location, instance, reason, memory_percent and release. reason is out_of_memory when the instance was using 90% or more of its memory limit at the reading before the restart, and restarted otherwise. memory_percent is the share of the limit that reading used, or null. We send it once for each restart, and we leave out a restart that follows a power action we sent, a wake from sleep or a release that is still on its way.
  • deployment.instance.replaced has the same keys, with reason set to memory for an instance whose memory stayed close to its limit, and to memory_limit for one that still ran an earlier memory limit.
  • deployment.alert.out_of_memory has location, instance, trigger, restarts, memory_percent and release. trigger is out_of_memory or repeated_restarts, and restarts is how often that instance restarted in the last hour. We send it at most once for a deployment in six hours.
  • deployment.memory.limit_changed has from_mb, to_mb, reason and release. reason is memory_high for a step up and memory_low for a step down.
  • deployment.alert.memory_at_limit has reason, location, instance, memory_percent, limit_mb, max_mb and release. reason says why the limit cannot grow: largest_limit (it is the largest limit the owner allows), no_room (a location has no room for the next limit) or spend_limit (a spend limit holds the deployment). max_mb is the largest limit. We send it at most once for a deployment in six hours.

instance is the instance's id, as the deployment's GET /instances lists it and the logs' instance filter takes it. Logs and metrics explains what each event means for your deployment, and Memory grows automatically explains the memory events.

We count a delivery as done when your endpoint answers with a 2xx status. Any other answer, a timeout after 10 seconds or a connection error is a failure, and we try again 5, 30 and 120 minutes after each failure: four attempts in all. We do not follow redirects, so a 3xx answer is a failure too. When the fourth attempt fails and the webhook has failed 10 times in a row, we turn it off. Fix the endpoint, then turn the webhook back on.

Before each attempt we check the address again, and the delivery goes to the address that check found, so a name that changes afterwards never receives it. When its name now leads to a private address or one of Coritan's, nothing is sent and the attempt counts as a failure.

The same event can arrive more than once, and an event whose delivery failed can arrive after the events that followed it. Make your handler safe to run twice: a deployment event's id, and a service event's event_id, stay the same across its deliveries, so you can drop one you have already handled. For a service, act on its status rather than on the event's name: give the customer what they bought while it is active, take it back otherwise, and skip an event whose occurred_at is older than the last one you applied for that service_id.

Each delivery carries an X-Webhook-Signature header in the form t=<unix time>,v1=<signature>. The signature is the hex HMAC-SHA256, keyed with the webhook's signing secret, of the value of t, a full stop, and the body exactly as it was sent. Check it over the bytes that arrived, before you parse the JSON, and refuse a delivery whose signature does not match. Each attempt is signed as it is made, so a retry carries a new t, and your endpoint can refuse a t more than a few minutes old.

Python
import hashlib, hmac

def verified(secret: str, header: str, body: bytes) -> bool:
    parts = dict(item.split("=", 1) for item in header.split(","))
    expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

We show the secret only when you create the webhook and when you rotate its secret, and no page or answer shows it later. If you have lost it, rotate it (Rotate the signing secret).

The webhook turned itself off
Its deliveries failed 10 times in a row, and its page says so. Read the Answer of its failed deliveries (Check a webhook's deliveries), make sure the endpoint answers 2xx within 10 seconds, then turn the webhook back on.
Nothing arrives for an event you subscribed to
Check that its name is in a table under Events. A name typed in Other events that we do not send receives nothing.
No service.terminated arrives when a customer cancels
A service cancelled at the end of its term sends service.cancelled with its termination_date, and keeps running. service.terminated follows once that date has passed (Service events).
A service.suspended arrives after the service.unsuspended that followed it
Its first delivery failed, and the retry came later. Compare its occurred_at with the last event you applied for the service, and skip the older one (Deliveries and retries).
URL points at the platform itself
The endpoint is on Coritan's own network, such as a brand's hostname, the API or a deployment that is not your organization's. Send the events to a server of your own, or to one of your organization's active deployments on the default https port (Send events to your own deployment). Hostname targets a private or reserved name is the same for a private address.
Every signature check fails
Check the signature over the body exactly as it arrived, before any parsing, and with the newest secret: rotating it stops the old one at once. If you no longer have the secret, rotate it (Rotate the signing secret).
404 Webhook not found
The webhook ID belongs to another organization or the webhook was deleted.

Every route needs an owner's or admin's access token, sent as Authorization: Bearer $CORITAN_TOKEN.

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/webhooks" \
  -H "Authorization: Bearer $CORITAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/hooks/coritan/7Hq2xV9pLk", "events": ["invoice.paid", "service.ordered"]}'
Route What it does
GET /api/v1/orgs/{org_slug}/webhooks Lists the webhooks.
POST /api/v1/orgs/{org_slug}/webhooks Creates one from url and events, and answers 201 with its signing_secret.
GET /api/v1/orgs/{org_slug}/webhooks/{webhook_id} Reads one webhook, as a row of the list reads. A webhook of another organization answers 404 Webhook not found.
PATCH /api/v1/orgs/{org_slug}/webhooks/{webhook_id} Changes any of url, events and is_active.
POST /api/v1/orgs/{org_slug}/webhooks/{webhook_id}/rotate-secret Replaces the signing secret, and answers 200 with the new signing_secret. The old one stops at once.
DELETE /api/v1/orgs/{org_slug}/webhooks/{webhook_id} Deletes the webhook and its deliveries.
GET /api/v1/orgs/{org_slug}/webhooks/{webhook_id}/deliveries Lists the webhook's deliveries, newest first.

Each webhook in an answer has id, url, events, is_active, failure_count (failures in a row), last_triggered_at (the last successful delivery) and created_at. Only the answers of POST /webhooks and rotate-secret add signing_secret, so keep it from them. A url that is not https, or that points at a private address or at Coritan's own network other than your organization's own deployment, answers 422 with the reason, on create and on PATCH.

Shell
curl "https://api.coritan.com/api/v1/orgs/acme/webhooks/42/deliveries?status_filter=failed&limit=25&with_total=true" \
  -H "Authorization: Bearer $CORITAN_TOKEN"

status_filter takes pending, sending, delivered, failed or cancelled. limit takes 1–200 and defaults to 50, and offset skips that many of the newest. Without with_total the answer is a list; with with_total=true it is an object with items, total, limit, offset and counts, the number of deliveries in each status whatever the filter. A webhook of another organization answers 404 Webhook not found.

Each delivery has id, event_type, status, response_status (the endpoint's HTTP status, or null when it gave none), attempt_count, delivered_at, next_retry_at and created_at. attempt_count numbers the attempt being made: on a delivered one it is the attempt that got through, and on any other it is one more than the attempts made so far. No answer carries the body we sent or the endpoint's answer.

API operations on this page

MethodPathWhat it does
GET/api/v1/orgs/{org_slug}/webhooksList webhooks
POST/api/v1/orgs/{org_slug}/webhooksCreate webhook
GET/api/v1/orgs/{org_slug}/webhooks/{webhook_id}One webhook, as GET /webhooks lists it
PATCH/api/v1/orgs/{org_slug}/webhooks/{webhook_id}A new address is checked as on create (422)
DELETE/api/v1/orgs/{org_slug}/webhooks/{webhook_id}Delete webhook
GET/api/v1/orgs/{org_slug}/webhooks/{webhook_id}/deliveriesList webhook deliveries
POST/api/v1/orgs/{org_slug}/webhooks/{webhook_id}/rotate-secretA fresh signingsecret, in this answer only