Ivy on File
Setup and use mandates to charge users multiple times
Create a mandate for recurring payments with PIS
You can embed setting up a mandate to be used for recurring charges within a one-off payment flow. This simplifies the process in your system, as it removes the need to build extra confirmation screens or integrate new Ivy endpoints.
On a high level, the flow is the following:
- Checkout Creation: Add a
mandate
object to your Checkout Create request, set the fieldsetup
totrue
, pass areferenceId
for the mandate to be able to associate the respectivemandate_setup_succeeded
andmandate_setup_failed
webhook events with your initial request later on. Additionally you need to pass auserNotificationEmail
to the mandate object which is used to be displayed during the checkout when requesting to setup a mandate (you need to notify the user about the respective events using this email address as well). - You can pass an optional object
additionalDisplayInformation
to show a cadence and amount for the mandate to your customers.- Keep in mind that both the object is optional, and either of price and cadence. But if you provide an amount, you also have to provide the currency.
- Cadence can be any of the following:
BI_WEEKLY, WEEKLY, MONTHLY, QUARTERLY, SEMI_ANNUAL, ANNUAL, ON_DEMAND
- See how showing these values looks in the checkout in the screenshots below.
- Proceed the same way as you would for a usual payment flow with Checkout Sessions and send your user to Ivy's hosted Checkout flow.
// Recommended Configuration
{
"price": {
"totalNet": 100,
"vat": 19,
"total": 119,
"currency": "EUR"
},
"referenceId": "my-unique-checkout-reference-id",
"successCallbackUrl": "https://my-website.com/success",
"errorCallbackUrl": "https://my-website.com/try-again",
"...": "...",
"mandate": {
"setup": true, // needs to be true to setup a mandate
"referenceId": "my-unique-mandate-reference",
"userNotificationEmail": "the-email-address-of-your-user-to-send-notifications-to",
"accountHolderName": "the-account-holder-name",
"additionalDisplayInformation": {
"cadence": "MONTHLY",
"price": {
"amount": 9.99,
"currency": "EUR",
}
}
}
}
- Within the Payment flow, users confirm to set up a mandate on the Ivy screens.
- Following a successful payment authorization that starts with the
CheckoutSession
, anOrder
will be created. Alongside thisOrder
, a mandate will be set up, but it will be set up in a way that does not happen immediately but rather separately and without blocking the payment authorisation process.
Handling mandate related webhook events
We are emitting several mandate related webhook events while setting up and finalizing a mandate. To get notified via changes to your mandate, you should subscribe to these via a webhook.
Currently we emit the following events:
mandate_setup_started
mandate_setup_succeeded
mandate_setup_failed
mandate_revoked
Mandate setup started
When initiating a Checkout Create with a mandate as described above, we will notify you when the mandate setup has started. You can receive the mandate_setup_started
event to confirm that starting the mandate setup was successful and will get notified further down the line once the mandate has been finalized via the mandate_setup_succeeded
or the mandate_setup_failed
event.
{
"id": "c8878bf1-ea3c-47da-aff2-4f624399b3d0",
"type": "mandate_setup_started",
"payload": {
"referenceId": "my-unique-mandate-reference-id", // You are setting this in the mandate object in /checkout/session/create
"reference": "my-unique-checkout-reference-id", // You are setting this in /checkout/session/create
"signature": {
"ip": "127.0.0.1",
"token": "uniqueToken_ab12cd34",
"signedAt": "2024-07-24T16:38:37.240Z"
},
"userNotificationEmail": "[email protected]",
"creditor": {
"id": "ABC1289113189389",
"name": "Creditor Name",
"address": {
"street": "Example Street",
"city": "Example City",
"postalCode": "12345",
"country": "DE"
}
}
},
"date": "2024-07-24T16:38:48.621Z"
}
Successful mandate setup
Following a successful mandate setup, you can receive the mandate_setup_succeeded
webhook event to retrieve and store the mandate's id
which can be use to initiate charges afterwards. This webhook event contains all relevant mandate data that can be used to inform the user about the issuance and usage of the mandate as required by the SEPA regulation.
{
"id": "", // The id of the specific webhook event. Use this in the /webhook/trigger endpoint to re-send the webhook.
"type": "mandate_setup_succeeded",
"payload": {
"id": "", // the id of the mandate to be used in the future
"referenceId": "", // the reference provided during the initial checkout session under mandate.referenceId
"reference": "", // the mandate reference that will appear on the customer's bank statement
"signature": {
"ip": "",
"token": "",
"signedAt": ""
},
"creditor": {
"address": {
"street": "",
"city": "",
"postalCode": "",
"country": "" // alpha2 country code
},
"id": "",
"name": ""
},
"debtor": { // debtor account information of the account the mandate has been issued for
"account": {
"iban": "",
"accountHolderName": "",
"bic": ""
}
}
},
"date":"datetime" // The datetime timestamp of the webhook
}
Failed mandate setup
When a mandate setup fails, you can get notified using the mandate_setup_failed
webhook event to get more insights, why the mandate setup failed.
{
"id": "", // The id of the specific webhook event. Use this in the /webhook/trigger endpoint to re-send the webhook.
"type": "mandate_setup_failed",
"payload": {
"error": {
"code": "",
"message": ""
},
"referenceId": "", // the reference provided during the initial checkout session under mandate.referenceId
},
"date":"datetime" // The datetime timestamp of the webhook
}
Mandate revocation
When a charge is reversed by a customer or we detected that it's no longer valid due to any other reason, we inform you about the mandate being revoked. In this case this mandate can't be used any longer to create a charge and a new mandate needs to be setup in case you want to create further charges for the respective customer.
{
"id": "", // The id of the specific webhook event. Use this in the /webhook/trigger endpoint to re-send the webhook.
"type": "mandate_revoked",
"payload": {
"id": "", // the id of the mandate
"referenceId": "", // the reference provided during the initial checkout session under mandate.referenceId
"revokedAt": "" // the date time the mandate has been revoked
},
"date":"datetime" // The datetime timestamp of the webhook
}
Example Webhook Handler
const express = require('express')
const router = express.Router()
const { createHmac } = require('crypto')
const config = require('./config') // assuming your config is in a file named config.js
function sign(data, secret) {
const hmac = createHmac('sha256', secret)
hmac.update(JSON.stringify(data))
return hmac.digest('hex')
}
router.post('/webhooks/ivy/mandate', (req, res) => {
const data = req.body
// verify signature
const secret = config.IVY_WEBHOOK_SIGNING_SECRET
const expectedSignature = sign(data, secret)
const signature = req.get('X-Ivy-Signature')
if (signature !== expectedSignature) {
throw new Error('Invalid signature')
}
// check for type of event
switch (data.type) {
case 'mandate_setup_started':
// take mandate reference and creditor information and show the pending state
case 'mandate_setup_succeeded':
// take data.payload.referenceId to identify the original request
// use / store data.payload.id for further usage of the mandate
break;
case 'mandate_setup_failed':
// take data.payload.referenceId to identify the original request
// if appropriate e.g. let the customer know that they should try again or not
// you can for example adjust your communication based on
// data.payload.error.code
// and
// data.payload.error.message
break;
case 'mandate_revoked':
// take data.id and mark this mandate as not being usable anymore
// you might want / need to inform your customer and ask the next time for giving consent
// to set up a new mandate
break;
default:
// handle unknown status
throw new Error('Unknown mandate event type')
}
res.send({ success: true })
})
module.exports = router
<?php
// Assuming you have a config.php file with your configurations
require_once 'config.php';
// Function to sign data
function sign($data, $secret) {
$hmac = hash_hmac('sha256', json_encode($data), $secret);
return $hmac;
}
// Check if the request is a POST request
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Get JSON payload from the request body
$json = file_get_contents('php://input');
$data = json_decode($json, true);
// Verify signature
$secret = IVY_WEBHOOK_SIGNING_SECRET; // Defined in config.php
$expectedSignature = sign($data, $secret);
$signature = $_SERVER['HTTP_X_IVY_SIGNATURE'] ?? '';
if ($signature !== $expectedSignature) {
throw new Exception('Invalid signature');
}
// check for type of event
switch ($data['type']) {
case 'mandate_setup_started':
// do something
case 'mandate_setup_succeeded':
// take data.payload.referenceId to identify the original request
// use / store data.payload.id for further usage of the mandate
break;
case 'mandate_setup_failed':
// take data.payload.referenceId to identify the original request
// if appropriate e.g. let the customer know that they should try again or not
// you can for example adjust your communication based on
// data.payload.error.code
// and
// data.payload.error.message
break;
case 'mandate_revoked':
// take data.id and mark this mandate as not being usable anymore
// you might want / need to inform your customer and ask the next time for giving consent
// to set up a new mandate
break;
default:
// handle unknown status
throw new Exception('Unknown mandate event type')
}
// Send success response
header('Content-Type: application/json');
echo json_encode(['success' => true]);
} else {
// Handle non-POST requests here
header('HTTP/1.1 405 Method Not Allowed');
exit;
}
?>
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Contracts\Service\Attribute\Required;
use App\Service\SignatureVerifier; // Assuming you have a service for signature verification
class WebhookController extends AbstractController
{
private $signatureVerifier;
/**
* @Required
*/
public function setSignatureVerifier(SignatureVerifier $signatureVerifier): void
{
$this->signatureVerifier = $signatureVerifier;
}
/**
* @Route("/webhooks/ivy/mandate", name="ivy_mandate_webhook", methods={"POST"})
*/
public function ivyWebhook(Request $request): Response
{
$data = json_decode($request->getContent(), true);
// Verify signature
$signature = $request->headers->get('X-Ivy-Signature');
if (!$this->signatureVerifier->verifySignature($data, $signature)) {
throw new HttpException(400, 'Invalid signature');
}
// check for type of event
switch ($data['type']) {
case 'mandate_setup_started':
// do something
break;
case 'mandate_setup_succeeded':
// take data.payload.referenceId to identify the original request
// use / store data.payload.id for further usage of the mandate
break;
case 'mandate_setup_failed':
// take data.payload.referenceId to identify the original request
// if appropriate e.g. let the customer know that they should try again or not
// you can for example adjust your communication based on
// data.payload.error.code
// and
// data.payload.error.message
break;
case 'mandate_revoked':
// take data.id and mark this mandate as not being usable anymore
// you might want / need to inform your customer and ask the next time for giving consent
// to set up a new mandate
break;
default:
// handle unknown status
throw new HttpException(400, 'Unknown mandate event type')
}
return new Response(json_encode(['success' => true]), 200, ['Content-Type' => 'application/json']);
}
}
from fastapi import FastAPI, Request, HTTPException
import hashlib
import hmac
import json
app = FastAPI()
# Assuming you have a configuration module or environment variables
# For example, using environment variables:
import os
IVY_WEBHOOK_SIGNING_SECRET = os.environ.get("IVY_WEBHOOK_SIGNING_SECRET")
def verify_signature(data: str, signature: str, secret: str) -> bool:
"""Verify HMAC signature."""
hmac_signature = hmac.new(secret.encode(), data.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(hmac_signature, signature)
@app.post("/webhooks/ivy/mandate")
async def ivy_webhook(request: Request):
body = await request.body()
body_str = body.decode("utf-8")
# Retrieve the signature from the headers
signature = request.headers.get("x-ivy-signature")
# Verify signature
if not verify_signature(body_str, signature, IVY_WEBHOOK_SIGNING_SECRET):
raise HTTPException(status_code=400, detail="Invalid signature")
data = json.loads(body_str)
# Check event type
type = data.get("type")
if type not in ["mandate_setup_started", "mandate_setup_succeeded", "mandate_setup_failed", "mandate_revoked"]:
raise HTTPException(status_code=400, detail="Unknown mandate event type")
# Handle the statuses here
# For example, you can log them or perform actions based on the status
print(f"Mandate status: {status}")
return {"success": True}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Charge a user using a mandate
With a valid mandate id
, you can charge users without any user interaction. This simplifies charging customers for e.g. one-click user experiences or if you want to trigger recurring payments.
The following steps are necessary:
- Create a new
Charge
with the/charge/create
endpoint. Provide the mandate'smandateId
you previously received (as explained in the preceding paragraph), the price defined bytotal
,currency
and a uniquereferenceId
for the charge you can use to identify later on when receiving webhooks . AnidempotencyKey
is mandatory for this call to prevent you from accidentally creating multiple charges for a single charge intent.
This way, the associated bank account is charged.curl -X POST "https://api.sand.getivy.de/api/service/charge/create" \ -H "Content-Type: application/json" \ -H "X-Ivy-Api-Key: {Your Ivy Api Key}" \ -d '{ "mandateId": "provide_mandate_id_here" "price": { "total": 100.23 "currency": "EUR" }, "idempotencyKey": "a_key_to_uniquely_identify_your_request", "referenceId": "unique_reference_id" }'
- Case 1: the
Charge
request failed- A Validation API Error with the reason for the failure is returned by the Charge Create endpoint
- Case 2: the
Charge
request succeeded- A charge to the user's bank account has been submitted and is being processed.
- A corresponding
Order
object is created withstatus: processing
, retrievable with the Order API. Aorder_created
webhook is sent.
Special case: Charge a user with a mandate and subaccount
Similar to the regular charge: With a valid mandate id
, you can charge users without any user interaction. By additionally providing a subaccountId
you can enhance the user experience further, since your subaccount will be mentioned on the bank statement. Making it easier for a user to recognize charges.
Prerequisites - Set up a subaccount
Before you can create charges using a subaccount, you have to create one first. You do this by calling the /subaccount/create endpoint and provide a legalName
, mcc
(Merchant Category Code) and websiteUrl
(if the merchant type is not offline)
curl --request POST \
--url https://api.getivy.de/api/service/subaccount/create \
--header 'X-Ivy-Api-Key: my-ivy-api-key' \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '
{
"legalName": "Example Subaccount GmbH",
"mcc": "4188",
"websiteUrl": "https://www.example-subaccount.com"
}
'
If the creation was successful you will receive a response like this:
{
"id": "80947008-afec-43fc-a691-e2eb0069653f",
"legalName": "Example Subaccount GmbH",
"ownerId": "6567433072e99800ed899ddf",
"mcc": "4188",
"status": "active",
"updatedAt": "2024-07-24T17:40:26.404Z",
"createdAt": "2024-07-24T17:40:26.404Z",
}
Subaccounts can not be used right away - reach out to your integration partner!
You can now use the subaccount id
you received in the response, to create charges. These charges will then be enriched with the Subaccount information in the bank statement.
Bank statement without subaccount usage:
${merchant.displayName} - ${order.referenceId} // e.g. My Custom Shop - ORD1931829431819
Bank statment with subaccount usage:
${subaccount.legalName} - ${order.referenceId} // e.g. My Custom Subaccount - ORD1931829431819
Example for a charge with subaccount id
curl --request POST \
--url https://api.getivy.de/api/service/charge/create \
--header 'X-Ivy-Api-Key: my-ivy-api-key' \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '
{
"price": {
"currency": "EUR",
"total": 10.95
},
"subaccountId": "80947008-afec-43fc-a691-e2eb0069653f",
"mandateId": "7cfa96d2-85de-40ae-b167-079fefee7d5b",
"referenceId": "MY-UNIQUE-REFERENCE-ID-1234",
"idempotencyKey": "e66588b9-908f-46a3-8b6e-4c25853337ad"
}
'
Handle Status Updates
Ivy uses the Order
object to keep track of the payment status and informs you of any creation and update by sending order_created
and order_updated
webhooks.
As the charge to the bankToken is in most cases a Direct Debit payment, it can take up to 14 business days to receive notification on the success or failure of a charge. However, the average is five business days.
Case | order.status | What to do |
---|---|---|
Submitted Payment | processing | Nothing |
Successful Payment | paid | Fulfill the order / service |
Failed Payment | failed | Cancel the order / service |
Disputed Payment | disputed | Cancel the order / service |
Example Webhook Handler
const express = require('express')
const router = express.Router()
const { createHmac } = require('crypto')
const config = require('./config') // assuming your config is in a file named config.js
function sign(data, secret) {
const hmac = createHmac('sha256', secret)
hmac.update(JSON.stringify(data))
return hmac.digest('hex')
}
router.post('/webhooks/ivy', (req, res) => {
const data = req.body
// verify signature
const secret = config.IVY_WEBHOOK_SIGNING_SECRET
const expectedSignature = sign(data, secret)
const signature = req.get('X-Ivy-Signature')
if (signature !== expectedSignature) {
throw new Error('Invalid signature')
}
// check if correct event
if (data.type !== "order_created" && data.type !== "order_updated") {
throw new Error('Expected different Webhook Event')
}
// check order.status
switch (data.payload.status) {
case 'processing':
// do nothing
break;
case 'paid':
// fulfill the order / service
break;
case 'failed':
// cancel the order / service
break;
case 'disputed':
// cancel the order / service
default:
// handle unknown status
throw new Error('Unknown status')
}
res.send({ success: true })
})
module.exports = router
<?php
// Assuming you have a config.php file with your configurations
require_once 'config.php';
// Function to sign data
function sign($data, $secret) {
$hmac = hash_hmac('sha256', json_encode($data), $secret);
return $hmac;
}
// Check if the request is a POST request
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Get JSON payload from the request body
$json = file_get_contents('php://input');
$data = json_decode($json, true);
// Verify signature
$secret = IVY_WEBHOOK_SIGNING_SECRET; // Defined in config.php
$expectedSignature = sign($data, $secret);
$signature = $_SERVER['HTTP_X_IVY_SIGNATURE'] ?? '';
if ($signature !== $expectedSignature) {
throw new Exception('Invalid signature');
}
// Check if correct event
if ($data['type'] !== "order_created" && $data['type'] !== "order_updated") {
throw new Exception('Expected different Webhook Event');
}
// Check order.status
switch ($data['payload']['status']) {
case 'processing':
// Do nothing
break;
case 'paid':
// Fulfill the order/service
break;
case 'failed':
// Cancel the order/service
break;
case 'disputed':
// Handle dispute
break;
default:
// Handle unknown status
throw new Exception('Unknown status');
}
// Send success response
header('Content-Type: application/json');
echo json_encode(['success' => true]);
} else {
// Handle non-POST requests here
header('HTTP/1.1 405 Method Not Allowed');
exit;
}
?>
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Contracts\Service\Attribute\Required;
use App\Service\SignatureVerifier; // Assuming you have a service for signature verification
class WebhookController extends AbstractController
{
private $signatureVerifier;
/**
* @Required
*/
public function setSignatureVerifier(SignatureVerifier $signatureVerifier): void
{
$this->signatureVerifier = $signatureVerifier;
}
/**
* @Route("/webhooks/ivy", name="ivy_webhook", methods={"POST"})
*/
public function ivyWebhook(Request $request): Response
{
$data = json_decode($request->getContent(), true);
// Verify signature
$signature = $request->headers->get('X-Ivy-Signature');
if (!$this->signatureVerifier->verifySignature($data, $signature)) {
throw new HttpException(400, 'Invalid signature');
}
// Check if correct event
if ($data['type'] !== "order_created" && $data['type'] !== "order_updated") {
throw new HttpException(400, 'Expected different Webhook Event');
}
// Check order.status
switch ($data['payload']['status']) {
case 'processing':
// Do nothing
break;
case 'paid':
// Fulfill the order/service
break;
case 'failed':
// Cancel the order/service
break;
case 'disputed':
// Handle dispute
break;
default:
// Handle unknown status
throw new HttpException(400, 'Unknown status');
}
return new Response(json_encode(['success' => true]), 200, ['Content-Type' => 'application/json']);
}
}
from fastapi import FastAPI, Request, HTTPException
import hashlib
import hmac
import json
app = FastAPI()
# Assuming you have a configuration module or environment variables
# For example, using environment variables:
import os
IVY_WEBHOOK_SIGNING_SECRET = os.environ.get("IVY_WEBHOOK_SIGNING_SECRET")
def verify_signature(data: str, signature: str, secret: str) -> bool:
"""Verify HMAC signature."""
hmac_signature = hmac.new(secret.encode(), data.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(hmac_signature, signature)
@app.post("/webhooks/ivy")
async def ivy_webhook(request: Request):
body = await request.body()
body_str = body.decode("utf-8")
# Retrieve the signature from the headers
signature = request.headers.get("x-ivy-signature")
# Verify signature
if not verify_signature(body_str, signature, IVY_WEBHOOK_SIGNING_SECRET):
raise HTTPException(status_code=400, detail="Invalid signature")
data = json.loads(body_str)
# Check if correct event
if data.get("type") not in ["order_created", "order_updated"]:
raise HTTPException(status_code=400, detail="Expected different Webhook Event")
# Check order.status
status = data.get("payload", {}).get("status")
if status not in ["processing", "paid", "failed", "disputed"]:
raise HTTPException(status_code=400, detail="Unknown status")
# Handle the statuses here
# For example, you can log them or perform actions based on the status
print(f"Order status: {status}")
return {"success": True}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Disputed Payments
Direct Debit provides a dispute process for customers to dispute payments.
Customers can dispute a payment through their bank on a “no questions asked” basis up to eight weeks after their account is debited. Any disputes within this period are automatically honored.
After eight weeks and up to 13 months, a customer can only dispute a payment with their bank if the debit is considered unauthorized. If this occurs, Ivy automatically provides the bank with the mandate that the customer approved. This does not guarantee cancellation of the dispute; the bank can still decide that the debit was unauthorized and the customer is entitled to a refund.
A dispute can also occur if the bank is unable to debit the customer’s account because of an issue (for example, the account is frozen or has insufficient funds), but has already provided the funds to make the charge successful. If this occurs, the bank reclaims the funds in the form of a dispute.
Disputes are final and there is no process for appeal. If a user successfully disputes a payment, you must contact them if you want to resolve the situation. If you’re able to come to an arrangement and your user is willing to return the funds to you, they must make a new payment.
In case of a dispute, the order.status
changes to disputed
, a order_updated
webhook is sent and the amount of the initial payment + a dispute fee is being deducted from the Ivy settlement account.
Simulating Failed Mandate Setups in Sandbox
Simulating failure
Mandate setup failures can occur for various reasons, such as incorrect account details, or technical issues. To test how your system handles these scenarios, you can use the sandbox environment with a mandate referenceId ending in -fail-setup
(e.g., abcdefg123456-fail-setup
).
As the mandate setup happens when the checkout session is initiated, you have to adapt your first call to create the checkout session Checkout Create
POST /api/checkout/session/create
{
"price": {
"totalNet": 100,
"vat": 19,
"total": 119,
"currency": "EUR"
},
"referenceId": "my-unique-checkout-reference-id",
"successCallbackUrl": "https://my-website.com/success",
"errorCallbackUrl": "https://my-website.com/try-again",
"...": "...",
"mandate": {
"setup": true, // needs to be true to setup a mandate
"referenceId": "unique-reference-id-fail-setup", // make sure this ends in -fail-setup
"userNotificationEmail": "the-email-address-of-your-user-to-send-notifications-to",
"accountHolderName": "the-account-holder-name"
}
}
This will immediately fail the mandate setup even though the PIS payment might succeed. After a short while you will receive an mandate_setup_failed
webhook event, indicating that the mandate setup has failed.
Simulating Failed and Disputed Payments in Sandbox
Simulating failure
Failing charges can happen for several reasons as e.g. a closed bank account, processing issues on the bank side etc. These are expected to happen before the payment is actually settled. In order to test this scenario you can use our sandbox environment using a referenceId
ending with -fail
(e.g. abcdefg123456-fail
):
POST /api/service/charge/create
{
"mandateId": "<the mandateid you received with the mandate_setup_succeed webhook event>",
"referenceId": "any string you like ending with -fail",
"idempotencyKey: "any string you want to use",
"price": {
"total": 1,
"currency": "EUR"
}
}
This will first accept the charge, giving you an order_created
webhook event. After a short while you will receive an order_updated
webhook event, indicating that the order failed.
Simulating dispute / chargeback
Disputed charges can happen after a payment has been settled as they represent as result of a customer triggered chargeback. The consequence of this is, that the order will be marked as disputed and the mandate will be revoked.
In order to test this scenario you can use our sandbox environment using a referenceId
ending with -dispute
(e.g. abcdefg123456-dispute
):
POST /api/service/charge/create
{
"mandateId": "<the mandateid you received with the mandate_setup_succeed webhook event>",
"referenceId": "any string you like ending with -dispute",
"idempotencyKey: "any string you want to use",
"price": {
"total": 1,
"currency": "EUR"
}
}
This will first accept the charge, giving you an order_created
webhook event. After a short while you will receive an order_updated
webhook event, indicating that the order has been paid. After another short while you will receive an order_updated
event, indicating that the order has been disputed and also you will receive a mandate_revoked
webhook event, indicating that the mandate has been revoked and should not be used any more (in fact you won't be able to use it for any further charges in this case)
Updated 5 months ago