Logocowtic
Public API

Event cancellation

Cancel an event through the Public API and optionally refund its zahls.ch payments

Cancelling an event through the API does exactly what the dashboard does: the event is set to CANCELLED, its pending and completed orders are cancelled, inventory is released and issued tickets are voided. Refunding the money is a separate, explicit choice.

Refunds are opt-in

refund defaults to false. A request without a body, or with "refund": false, cancels the event and moves no money. Only "refund": true queues refunds.

Authentication and scopes

All three operations use the same organization API key as the rest of the API — see Authentication.

OperationMethod and pathScope
Cancel an eventPOST /events/{eventId}/cancelevents:cancel
Read refund statusGET /events/{eventId}/refundsevents:read
Retry failed refundsPOST /events/{eventId}/refunds/retryevents:cancel

A key may only reach its own organization's events. An event belonging to another organization answers 404, never 403, so key holders cannot probe for event ids.

Cancel an event

Idempotency-Key is required. See Retries and idempotency below.

curl -X POST https://api.cowtic.com/api/v1/events/evt_2f8a/cancel \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{ "refund": true }'
{
  "eventId": "evt_2f8a",
  "status": "CANCELLED",
  "cancelledOrderIds": ["ord_91c", "ord_44b", "ord_07e"],
  "failedOrderIds": [],
  "refunds": {
    "queued": 2,
    "queuedOrderIds": ["ord_91c", "ord_44b"],
    "skipped": {
      "refundable": 0,
      "cancelled_earlier": 1,
      "unpaid": 3,
      "free": 1,
      "manual_payment": 0,
      "refunded": 0,
      "refund_pending": 0,
      "refund_failed": 0,
      "refund_unknown": 0,
      "refund_unsupported": 0
    }
  },
  "refundStatusUrl": "/api/v1/events/evt_2f8a/refunds"
}
FieldMeaning
cancelledOrderIdsOrders cancelled by this request. Empty on a repeat — they were already cancelled.
failedOrderIdsOrders that could not be cancelled. They keep their tickets and are never refunded, so surface them to an operator.
refundsnull when refund was false. Otherwise what this request handed to the refund queue.
refunds.queuedOrderIdsOrders whose refund is now queued.
refunds.skippedOrders left alone, counted by eligibility.
refundStatusUrlPath on this API host to poll for the per-order outcome.

Without a refund the call is shorter:

curl -X POST https://api.cowtic.com/api/v1/events/evt_2f8a/cancel \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Idempotency-Key: 4f1d0f8c-7a1e-4d51-9a1b-0d1f0b2d9a77"
{
  "eventId": "evt_2f8a",
  "status": "CANCELLED",
  "cancelledOrderIds": ["ord_91c", "ord_44b", "ord_07e"],
  "failedOrderIds": [],
  "refunds": null,
  "refundStatusUrl": "/api/v1/events/evt_2f8a/refunds"
}

Refunds are asynchronous

POST /events/{eventId}/cancel returns as soon as the orders are cancelled and the refunds are queued. Each refund is then sent to zahls.ch by a background worker. A 200 means queued, not refunded — read refundStatusUrl for the outcome.

Which orders get refunded

Only orders that were actually paid through zahls.ch and have not been refunded yet. Every other order is reported under refunds.skipped with the reason:

EligibilityMeaning
refundableCompleted order with a captured zahls.ch payment. Queued for refund.
cancelled_earlierCancelled before the event was, payment still captured. Never refunded automatically — it may already have been refunded by hand. Refund it from the order in the dashboard.
unpaidCheckout never completed. Nothing to refund.
freeFree or fully discounted order. Nothing to refund.
manual_paymentMarked as paid outside zahls.ch. Refund it through the channel that took the money.
refundedAlready refunded.
refund_pendingA refund is already queued or in flight.
refund_failedzahls.ch rejected the refund. Retryable.
refund_unknownzahls.ch never answered, so the refund may or may not have happened. Never retried automatically — check the transaction on zahls.ch first.
refund_unsupportedThe payment method cannot be refunded through zahls.ch.

Retries and idempotency

Idempotency-Key is required on POST /events/{eventId}/cancel and POST /events/{eventId}/refunds/retry; a request without it returns 400.

  • Repeating a request with the same key and the same body replays the stored response and runs nothing again. The replay carries Idempotency-Replayed: true.
  • Reusing a key with a different body returns 409.
  • Use a new key for a genuinely new attempt — for example after fixing your zahls.ch connection.

The operation is safe to repeat even with a fresh key: an event already CANCELLED is not cancelled twice, an order already cancelled is left alone, and a refund record already created is never duplicated, so no order is refunded twice. See Idempotency for the general rules.

Read the refund status

curl https://api.cowtic.com/api/v1/events/evt_2f8a/refunds \
  -H "X-Api-Key: YOUR_API_KEY"
{
  "eventStatus": "CANCELLED",
  "total": 2,
  "counts": { "PENDING": 0, "SUCCEEDED": 1, "FAILED": 1, "UNSUPPORTED": 0 },
  "stalePending": 0,
  "unknownOutcome": 0,
  "refundedAmount": 50,
  "currency": "CHF",
  "refunds": [
    {
      "id": "prf_7d1",
      "orderId": "ord_91c",
      "orderStatus": "CANCELLED",
      "customer": { "name": "Ada Guest", "email": "guest@example.com" },
      "status": "SUCCEEDED",
      "currency": "CHF",
      "paidAmount": 50,
      "requestedAmount": 50,
      "refundedAmount": 50,
      "failureReason": null,
      "attempts": 1,
      "lastAttemptAt": "2026-09-14T09:12:04.881Z",
      "updatedAt": "2026-09-14T09:12:05.102Z",
      "stale": false,
      "outcomeUnknown": false,
      "providerTransactionId": "5012345",
      "providerRefundId": "9001"
    },
    {
      "id": "prf_7d2",
      "orderId": "ord_44b",
      "orderStatus": "CANCELLED",
      "customer": { "name": "Blaise Guest", "email": "blaise@example.com" },
      "status": "FAILED",
      "currency": "CHF",
      "paidAmount": 12.5,
      "requestedAmount": 12.5,
      "refundedAmount": 0,
      "failureReason": "Refund limit reached for this account",
      "attempts": 1,
      "lastAttemptAt": "2026-09-14T09:12:04.902Z",
      "updatedAt": "2026-09-14T09:12:04.960Z",
      "stale": false,
      "outcomeUnknown": false,
      "providerTransactionId": "5012346",
      "providerRefundId": null
    }
  ]
}
FieldMeaning
countsRefund records by status. PENDING means queued or in flight.
stalePendingPENDING records no worker has touched for a while. A retry picks them up.
unknownOutcomeRefunds zahls.ch never confirmed. Check the transaction on zahls.ch before retrying.
refundedAmount / paidAmountAmounts in the order's currency, not minor units.
failureReasonThe message zahls.ch returned. Provider text only — no credentials are ever included.
providerTransactionId / providerRefundIdzahls.ch references for reconciliation.

Poll this endpoint until counts.PENDING reaches 0. A few seconds between polls is plenty; mind the rate limits.

Retry failed refunds

Queues the event's FAILED, UNSUPPORTED and abandoned refunds again. The event must already be cancelled, otherwise the call returns 400 with detail EVENT_NOT_CANCELLED.

curl -X POST https://api.cowtic.com/api/v1/events/evt_2f8a/refunds/retry \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Idempotency-Key: 9a3cbe44-2f9d-4f9e-8b4f-3c2d1a7b5e60"
{
  "queued": 1,
  "queuedOrderIds": ["ord_44b"],
  "skipped": {
    "refundable": 0,
    "cancelled_earlier": 0,
    "unpaid": 0,
    "free": 0,
    "manual_payment": 0,
    "refunded": 1,
    "refund_pending": 0,
    "refund_failed": 0,
    "refund_unknown": 0,
    "refund_unsupported": 0
  },
  "refundStatusUrl": "/api/v1/events/evt_2f8a/refunds"
}

Refunds counted under refund_unknown are deliberately left out: zahls.ch may already have executed them, so a person has to look before they are sent again.

Errors

Statustype suffixdetailWhat to do
400validation-errorrefund is not a booleanSend true or false, or omit the body
400bad-requestIdempotency-Key header is required for this endpointAdd the header
400bad-requestEVENT_NOT_CANCELLEDCancel the event before retrying its refunds
401unauthorizedMissing or invalid API keyCheck the key
403forbiddenAPI key lacks scope events:cancelGrant the scope, or rotate the key with it
404not-foundEVENT_NOT_FOUNDThe event does not exist in this organization
409idempotency-conflictKey reused with a different bodyUse a new key
503service-unavailableEVENT_PAYMENT_CREDENTIALS_REQUIREDConnect zahls.ch in the dashboard, or cancel without refund

Nothing is cancelled when a request fails validation, authorization or the zahls.ch check. Per-order failures are not errors: they come back in failedOrderIds and in the refund status. See Errors for the shared problem-details shape.

With the SDK

import { Cowtic } from "@cowtic/sdk";

const cowtic = new Cowtic({ apiKey: process.env.COWTIC_API_KEY! });

const { data: cancellation } = await cowtic.events.cancel("evt_2f8a", { refund: true });
console.log(cancellation.refunds?.queuedOrderIds);

const { data: status } = await cowtic.events.refunds("evt_2f8a");
if (status.counts.FAILED > 0) {
  await cowtic.events.retryRefunds("evt_2f8a");
}

The SDK generates an Idempotency-Key per call; pass your own with { idempotencyKey } to make a retry replay the first answer.

On this page