Skip to content

Register one group webhook endpoint

Register the endpoint before you take the first payment. An event raised while no endpoint of yours is subscribed to it reaches the retained NO_SUBSCRIBER state instead of your server, and enabling an endpoint afterwards delivers future events only.

Create a channel endpoint with POST /api/channel/webhook-endpoints. One endpoint and one signing secret receive events for every merchant already in the channel and every merchant provisioned later. Store the plaintext secret from the create response; list operations return only a hint.

create-webhook-endpoint.sh
curl --request POST \
--url https://staging.api.maxanapay.com/api/channel/webhook-endpoints \
--header 'Authorization: Bearer sk_test_replace_me' \
--header 'Content-Type: application/json' \
--data @create-webhook-endpoint.request.json

url is checked at registration, not at the first delivery, and the block above only works because it satisfies every rule. It has to be https, on the default port, without credentials in it, and its hostname has to resolve in DNS to an address on the public internet.

What you send What comes back
http://, or a port other than 443 422, naming url
a hostname DNS does not answer for 424 and Webhook endpoint hostname could not be resolved by DNS
a hostname resolving to a private or reserved address 400 and resolves to a private or reserved IP address

The 424 is the one that catches people, because it is the only refusal here that might be temporary and it reads as though it always is: the message says to retry later, and it is the same message whether the name does not exist or your own resolver blinked. Resolve the hostname yourself before deciding which you have — getent hosts your.receiver.example or dig +short your.receiver.example. An address back means the name is fine and the call is worth retrying; nothing back means it does not exist, and no number of retries will store it. The usual cause is a subdomain nobody has created yet: a parent domain resolving is no evidence that a name under it does.

Nothing beyond resolution is checked. Your receiver does not have to be running, and Maxana does not call it to find out — an endpoint whose handler you have not deployed registers, and its deliveries fail afterwards on their own terms.

secret is in that response and in no other. Every later read returns secret_hint instead, so an endpoint whose secret you did not store has to be rotated rather than recovered.

Only url is required. Omitting event_types does not subscribe you to nothing — it subscribes you to all 18 event types in the event vocabulary, which is what the operation reference’s own minimal example does. Send the list you want, as the example above does, unless you mean to receive everything.

Deduplicate by Maxana-Event-Id, and follow the shared webhook payload and delivery reference for signature verification, acknowledgement, event types, and retries.

Dispute lifecycle events include both Maxana’s dispute UUID in id and the provider’s identifier in provider_dispute_id. The deadline-warning event is the exception: dispute.evidence_deadline_approaching calls the provider value dispute_id and omits Maxana’s UUID. Correlate it by matching dispute_id to a stored lifecycle event’s provider_dispute_id, not its id. See the shared dispute payload field reference for the complete dispute.created, dispute.updated, dispute.resolved, and deadline-warning shapes.

An endpoint subscription is a snapshot. When Maxana adds an event type, update the endpoint with the complete list you want; event_types replaces rather than merges. Rotate a secret as a hard cutover.

Every delivery uses the shared event envelope, and the worked example shows one in full. Two identifiers inside it turn a delivery into one of your merchants’ orders.

data.object.merchant_id is the Maxana merchant id you received when you provisioned that merchant. It is on every event of every type, including the ones that describe no order at all, and it is the only attribution key a group endpoint needs. Maxana fills it in from the merchant the event was raised for and rejects any payload naming a different one, so it cannot disagree with the merchant whose activity produced the event.

data.object.external_order_id is the identifier you set when you created the checkout session. data.object.checkout_session_id is Maxana’s identifier for that same session, and the one the rest of the API speaks.

Both keys are present on every event that describes an order, carrying null where they do not apply: a payment taken without a checkout session has no session, and a session created without an external_order_id has no external id. A null is not an absent key, so you never have to tell “this payment carries no order id” apart from “this field is not being sent yet”.

Events Order identifiers
checkout.session.created, payment.created, payment.approved, payment.captured, payment.failed, payment.refunded external_order_id and checkout_session_id
dispute.created, dispute.updated, dispute.resolved, dispute.evidence_deadline_approaching external_order_id and checkout_session_id, naming the disputed order
text_to_pay.paid external_order_id, taken from the payment request, not from a session
subscription.created, subscription.charged, subscription.payment_failed, subscription.past_due, subscription.canceled None. Match on subscription_id.
merchant.onboarding_updated, merchant.payments_disabled None. They report a merchant, not an order.

A merchant in your channel can also own an endpoint of their own. Where one does, the same event reaches both endpoints, each signed with its own secret. Yours is an independent delivery rather than a copy of theirs, and each is acknowledged on its own.

A channel owns at most ten webhook endpoints. Creating an eleventh returns 400 with A channel can configure at most 10 webhook endpoints.

Disabled endpoints count toward that ten. DELETE /api/channel/webhook-endpoints/{id} stops delivery and keeps the row, because its delivery history is part of the record — it is not a removal, and it does not return the slot.

Point a disabled endpoint at the new destination instead of creating another one. That is the intended way to move a receiver, and it is why the limit does not become a dead end: a channel that has changed its receiver ten times reuses one of the ten it already owns rather than asking for an eleventh.

Moving one to a different host takes three steps, and the middle one is the step readers miss. Disable the endpoint first — replacing the URL of an endpoint that is still enabled is refused with Disable this webhook endpoint before replacing its URL. Then send the new URL with rotate_secret set to true; a host change without it is refused with Replacing a webhook endpoint with a different host requires rotate_secret=true so the old receiver cannot authenticate deliveries. Store the new plaintext secret from that response. Then enable the endpoint; delivery does not resume until you do. A URL change within the same host needs neither the rotation nor a new secret.

Channel webhooks do not create one endpoint or secret per merchant. Disabling and later enabling an endpoint resumes future deliveries only. If an event reached the retained NO_SUBSCRIBER state while no matching endpoint was active, enabling does not deliver it automatically. Contact Maxana and identify the retained event when explicit replay is required; replay is not a channel API operation.

Continue to Create a checkout session.