Skip to content

Webhooks

Our calls to YOU — the end of a session, and a chargeback raised against it. Format, retries, header, idempotency.

In the HTTP API reference, you are always the caller. Here it is us: we call your server to tell it what happened. So this page is not part of the OpenAPI contract, which describes what we serve.

It is also the only place you learn what the payment page never sees: the PSP reference of each operation, the card network a tranche ran on, and the card alias when the order asked to register your customer’s means of payment. The example below carries none: an order that registers the means is not split, since one mandate covers the whole order.

Two messages, one field to tell them apart

Section titled “Two messages, one field to tell them apart”

The event field says which one you are reading:

event when
session-ended a session finished
chargeback a PSP raised a dispute, weeks later

Switch on it from day one: we will be able to add fields without breaking anything on your side, and the ending you already handle will have nothing new to configure.

Declare one address once on your account and every session of yours is announced there. That is the simplest shape, and the one to reach for.

You can also give one session its own notificationUrl on POST /api/sessions, and that one wins. Useful when one flow has to be routed somewhere else. With neither an account address nor a session address, the ending is not announced.

Either way, the address is fixed when the session is created. An ending reaches the endpoint that was agreed when the order was placed, not the one you changed since.

We call once per ending, with one of these four statuses — nothing else is announced:

status what happened
PAID the order is paid
ABORTED your customer gave up
EXPIRED the session reached its time limit
AWAITED_REFUSED a lender refused the financing

AWAITED_REFUSED deserves a word. An Oney file can leave your customer waiting on the lender’s verdict, sometimes for days. If the lender accepts, the session ends PAID — the ending you already handle, just later. If it refuses, or if the wait reaches its bound, the session ends AWAITED_REFUSED: nothing was taken, captured is zero. Your customer left the page long ago, so this call is probably your only way to learn it.

{
"event": "session-ended",
"notificationId": "019f18f1-cbf4-762f-bd94-776f523c9ffb:PAID",
"orderId": "EP-123456",
"status": "PAID",
"total": { "amount": 100000, "currency": "EUR" },
"captured": { "amount": 100000, "currency": "EUR" },
"terms": [{ "label": "esf des Ménuires", "url": "https://shop.example/terms.pdf" }],
"termsAcceptedAt": 1730900400000,
"attempts": [
{
"attemptId": "019f18f2-6ceb-729c-98b4-d60271a718e0",
"method": "card",
"methodId": "card",
"cardScheme": "visa",
"provider": "dalenys",
"status": "CAPTURED",
"requested": { "amount": 60000, "currency": "EUR" },
"granted": { "amount": 60000, "currency": "EUR" },
"operations": [
{
"type": "authorization",
"status": "succeeded",
"amount": { "amount": 60000, "currency": "EUR" },
"providerRef": "A180635471",
"statusCode": "0000",
"statusMessage": "Successful Operation"
},
{
"type": "capture",
"status": "succeeded",
"amount": { "amount": 60000, "currency": "EUR" },
"providerRef": "A180635474",
"statusCode": "0000",
"statusMessage": "Successful Operation"
}
]
},
{
"attemptId": "019f18f3-418c-729c-98b4-e2d7be7c6163",
"method": "wero",
"methodId": "wero",
"provider": "payplug-retail",
"status": "CAPTURED",
"requested": { "amount": 40000, "currency": "EUR" },
"granted": { "amount": 40000, "currency": "EUR" },
"operations": [
{
"type": "payment",
"status": "succeeded",
"amount": { "amount": 40000, "currency": "EUR" },
"providerRef": "pay_6GEUtvaN0NH68vioAWb9LF"
}
]
}
]
}

A €1,000 order, paid in two tranches: €600 by card, then €400 with Wero. That is a split payment as your server sees it.

provider says whose dashboard the tranche’s providerRefs live in: a reference alone does not tell you which back-office to open. The tranche records it when it opens, so it is always present, and it never moves afterwards even if your configuration changes.

How many operations, and of which type, depends on the method and the PSP — the example above shows both shapes. A card at Dalenys plays out in two steps, an authorization then a capture, each with its own reference. A Wero payment settles in a single payment operation. So do not wait for a capture to know a tranche went through: the tranche’s own status says so, whichever path it took.

cardScheme gives the network — visa, cb, mastercard, amex… — as the PSP named it, not as the page guessed it. On a co-badged card it is the network the payment actually ran on: the one that decides your interchange, and the one your customer’s statement will show. Absent, it means the verdict named no network we could place — never that the tranche was not a card, which the method beside it already says.

terms and termsAcceptedAt hand you back your own documents and the instant your customer accepted them — the proof of a consent you asked for, given to the party who may one day have to produce it. terms is the list as it stood when they accepted, wording and links included, so it survives a catalogue that has moved since. termsAcceptedAt alone is absent when a payment page never gathered the consent: an honest silence rather than an invented hour, and a sign your integration composed no <payplug-terms>. Both absent means the session declared no terms.

statusCode and statusMessage carry the PSP’s verdict as the PSP said it: the code you can search their dashboard for, and the sentence beside it. We never recompose it, which has a visible consequence above — Dalenys names even its successes (0000 Successful Operation), PayPlug says nothing on a successful payment, and both fields are then simply absent. A failure our platform produced itself carries statusMessage alone: there is no PSP code to give, and we do not invent one.

We count a delivery as received on HTTP 200 with the body OK, in text/plain. A 2xx alone says something answered, not that you understood: an OK lost inside an error page is exactly what this rule catches.

Anything else — another status, another body, a network failure — is retried at t+1 second, t+1 minute, then t+5 minutes. After that we stop, and someone is told on our side.

We send the value you agreed with us on the authorization header, verbatim, scheme included. Compare the whole string, in constant time.

Never normalize one side only: stripping a Bearer off what you receive while your stored secret keeps it rejects every legitimate call, while looking perfectly healthy.

That secret is a bearer: it proves nothing about the body, and anyone who intercepts it can impersonate us. Your endpoint must therefore be https. We refuse a plain-http address when the session is created, except for hosts that cannot exist on the public internet (localhost, a container name), so local development stays possible.

notificationId does not change across retries: the same ending always carries the same id. If your acknowledgement is lost on the way back, we come again with that one. Key your work on it, or a retry will ship the order a second time.

Do not use orderId for this: one order can produce two endings — a session that expired, then a second one that got paid — and collapsing them would swallow the second, which is the one that took the money.

A chargeback is your customer’s bank taking the money back, weeks or months after the payment. We receive the dispute and relay it to you as it stands: nothing is handled automatically on our side.

These messages are on request: declare a chargeback address on your account. Without one you receive nothing, and that is a perfectly normal answer — a disputes team is rarely the one that handles the payment funnel, and we will not push chargebacks at an address that did not ask for them.

{
"event": "chargeback",
"notificationId": "A180635474:chargeback:2026-08-18:60000",
"orderId": "EP-123456",
"sessionId": "019f18f1-cbf4-762f-bd94-776f523c9ffb",
"attemptId": "019f18f2-6ceb-729c-98b4-d60271a718e0",
"transactionId": "019f18f2-73ee-729c-98b4-dd336f0beb22",
"type": "chargeback",
"amount": { "amount": 60000, "currency": "EUR" },
"provider": "dalenys",
"providerRef": "A180635474",
"reasonCode": "13.1",
"status": "First chargeback",
"date": "2026-08-18",
"validUntil": "2026-09-15"
}

type says which way the money went. chargeback is a debit: it left your account. representment is a credit: the dispute was settled in your favour and the money comes back. Reading amount alone would count both the same way.

The attachment is the point. At the PSP, a chargeback only names one of their own references. We resolve it to the order you know, the session it belonged to and the exact tranche the money moved on — so you can act without having to search. A chargeback we cannot attach to anything of yours is never sent: it is worked out by hand on our side.

validUntil is a deadline. Past that date the dispute is usually lost by default. That is why this delivery follows the same rules as an ending — OK on 200, retries at t+1 s, t+1 min, t+5 min — and why someone is told on our side if we cannot reach you.

Idempotency works the same way. notificationId is stable across retries, and a PSP resending the same chargeback does not produce a second message. Two chargebacks that differ by amount or by date on the same transaction, on the other hand, are two chargebacks, and both arrive.

What we do not do: change the session’s status, correct what it says it captured, or reduce what remains refundable. It stays exactly as it finished. A chargeback is accounted for outside this platform, and this message is all we do about it.