Callbacks
Callbacks are used to interact with adapters without tightly coupling them. There are three different types of callbacks registered on the order:
- Validations
- Hooks
- Notifications
Validations
We can request the order to be validated by calling the /validate
action on our order. The order service will first validate the order for the most basic requirements:
- At least one item in our cart.
- At least one payment to cover our order total if it is greater than zero.
- The sum of all payment amounts must exactly cover our order total.
It will then move on to all registered validation callbacks. These allow each adapter to add their own requirements for a valid order. An example could be a platform adapter that validates that the items in our cart are still in stock. Another example could be our voucher adapter validates that the voucher is still available for use. All these validations are called in parallel to perform the entire validation as quickly as possible.
Hooks
Hooks are used to enable adapters to have a say in, or react to any changes made to the order. If a hook is registered for any change made to a part of an order, the hook will be invoked and a response will be awaited before committing the change. The hook invokation can respond with a patch document with further changes made to the order, which can in turn trigger new hooks.
Currently subscribeTo
and target
supports:
-
/state
-
/consents
-
/customer
-
/cart
-
/shippings
-
/payments
They are handled in this order. First by subscribeTo
and secondly by target
.
Example hook
In this example, we've created a hook to monitor any changes to the order state. When triggered, the Example adapter will assess the change and respond with either 200 OK
to approve or 400 Bad Request
to deny.
{
"adapterId": "example_adapter",
"subscribeTo": "/state",
"invoke": "https://example-payment-adapter.norce.io/api/v1/callbacks/orders/{order_id}/state-changes"
}
In the next example, we set a hook for cart changes, targeting payments for updates. This is useful when the cart total changes, requiring a payment update. A simplified request and response follow. In this case, the previous order total was 100
, but after the cart update, the new total is 200
. The Example adapter responds by updating the payment amount to match the new total of 200
.
{
"adapterId": "example_adapter",
"subscribeTo": "/cart",
"target": "/payments",
"invoke": "https://example-payment-adapter.norce.io/api/v1/callbacks/orders/{order_id}/cart-changes"
}
POST /api/v1/callbacks/orders/<order_id>/cart-changes
X-Merchant: <merchant>
X-Channel: <channel>
Authorization: Bearer <token>
Host: example-payment-adapter.norce.io
{
"cart": {
"total": {
"includingVat": 200,
"excludingVat": 160
}
},
"payments": [
{
"id": "<paymentId>",
"adapterId": "example_adapter",
"name": "Example Payment",
"type": "default",
"amount": 100
}
],
"hooks": [
{
"adapterId": "example_adapter",
"subscribeTo": "/cart",
"target": "/payments",
"invoke": "https://example-payment-adapter.norce.io/api/v1/callbacks/orders/{order_id}/cart-changes"
}
],
"total": {
"includingVat": 200,
"excludingVat": 160
}
}
200 OK
Content-Type: application/json
[
{
"op": "replace",
"path": "payments/0",
"value": {
"id": "<paymentId>",
"adapterId": "example_adapter",
"amount": 200,
}
}
]
Multiple hooks example
In this example, we have multiple hooks on our order. When cart is updated we find two relevant hooks, one to our first_adapter
and one to our second_adapter
. The first_adapter
will receive the first hook call, and responds with changes to /shippings
. Now we add the third_adapter
to our list of hooks to be invoked, since there has been a change to something it subscribed to. The second_adapter
responds with changes to /payments
, this does not add any further hook to be called. Finally the third_adapter
hook is invoked, it receives an order containing the updated cart, the updated shippings, and the updated payments and can respond with further changes to payments.
[
{
"adapterId": "first_adapter",
"subscribeTo": "/cart",
"target": "/shippings",
"invoke": "https://..."
},
{
"adapterId": "second_adapter",
"subscribeTo": "/cart",
"target": "/payments",
"invoke": "https://..."
},
{
"adapterId": "third_adapter",
"subscribeTo": "/shippings",
"target": "/payments",
"invoke": "https://..."
}
]
Notifications
Notifications are triggered after a change is made. This is very useful when an adapter needs to be informed of a change but has no reason to delay the interaction with the user. The notifications are a bit more granular than hooks to support very specific events. Instead of subscribeTo
we have scope
wich can be any valid json path in the order. A notification also contains a json schema and the notification will only be triggered if this schema validates true to the value found at the json path provided in scope
.
Example Notification
In this example we're interest in a change to /state/currentStatus
that results in the value being changed to either accepted
or completed
.
{
"adapterId": "example_adapter",
"reference": "ca11ab1e-c0de-ca11-c0de-ca11edca11ed",
"description": "React to Accepted and Completed order.",
"scope": "/state/currentStatus",
"schema": {
"enum": [
"accepted",
"completed"
]
},
"invoke": "https://example-payment-adapter.norce.io/api/v1/callbacks/orders/{order_id}/state-changed"
}
Next we'll look at what happens on four different updates to the order.
-
Fist we update the state to
processing
. While this is a change to/state/currentStatus
it does not validate with our schema. No notification will be sent. -
Second we update the state to
accepted
. This is a change to our scope and it validates with our schema. A notification will be sent. - Third we update the order reference. This is not a change to our scope so it will not even attempt to validate our schema. No notification will be sent.
- Finally we update the state to `completed. This is again a change to our scope and it validates with our schema. A notification will be sent.
{
"state": {
"currentStatus": "processing",
"transitions": [
{
"status": "checkout",
"timeStamp": "2024-01-01T12:01:00.0000000Z"
},
{
"status": "processing",
"timeStamp": "2024-01-01T12:02:00.0000000Z"
}
]
},
"cart": {
"total": {
"includingVat": 200,
"excludingVat": 160
}
},
"notifications": [
{
"adapterId": "example_adapter",
"reference": "ca11ab1e-c0de-ca11-c0de-ca11edca11ed",
"description": "React to Completed order.",
"scope": "/state/currentStatus",
"schema": {
"enum": [
"completed"
]
},
"invoke": "https://example-payment-adapter.norce.io/api/v1/callbacks/orders/{order_id}/state-changed"
}
],
"total": {
"includingVat": 200,
"excludingVat": 160
}
}
{
"state": {
"currentStatus": "accepted",
"transitions": [
{
"status": "checkout",
"timeStamp": "2024-01-01T12:01:00.0000000Z"
},
{
"status": "processing",
"timeStamp": "2024-01-01T12:02:00.0000000Z"
},
{
"status": "accepted",
"timeStamp": "2024-01-01T12:03:00.0000000Z"
}
]
},
"cart": {
"total": {
"includingVat": 200,
"excludingVat": 160
}
},
"notifications": [
{
"adapterId": "example_adapter",
"reference": "ca11ab1e-c0de-ca11-c0de-ca11edca11ed",
"description": "React to Completed order.",
"scope": "/state/currentStatus",
"schema": {
"enum": [
"completed"
]
},
"invoke": "https://example-payment-adapter.norce.io/api/v1/callbacks/orders/{order_id}/state-changed"
}
],
"total": {
"includingVat": 200,
"excludingVat": 160
}
}
{
"reference": "new reference",
"state": {
"currentStatus": "completed",
"transitions": [
{
"status": "checkout",
"timeStamp": "2024-01-01T12:01:00.0000000Z"
},
{
"status": "processing",
"timeStamp": "2024-01-01T12:02:00.0000000Z"
},
{
"status": "accepted",
"timeStamp": "2024-01-01T12:03:00.0000000Z"
}
]
},
"cart": {
"total": {
"includingVat": 200,
"excludingVat": 160
}
},
"notifications": [
{
"adapterId": "example_adapter",
"reference": "ca11ab1e-c0de-ca11-c0de-ca11edca11ed",
"description": "React to Completed order.",
"scope": "/state/currentStatus",
"schema": {
"enum": [
"completed"
]
},
"invoke": "https://example-payment-adapter.norce.io/api/v1/callbacks/orders/{order_id}/state-changed"
}
],
"total": {
"includingVat": 200,
"excludingVat": 160
}
}
{
"reference": "new reference",
"state": {
"currentStatus": "completed",
"transitions": [
{
"status": "checkout",
"timeStamp": "2024-01-01T12:01:00.0000000Z"
},
{
"status": "processing",
"timeStamp": "2024-01-01T12:02:00.0000000Z"
},
{
"status": "accepted",
"timeStamp": "2024-01-01T12:03:00.0000000Z"
},
{
"status": "completed",
"timeStamp": "2024-01-01T12:04:00.0000000Z"
}
]
},
"cart": {
"total": {
"includingVat": 200,
"excludingVat": 160
}
},
"notifications": [
{
"adapterId": "example_adapter",
"reference": "ca11ab1e-c0de-ca11-c0de-ca11edca11ed",
"description": "React to Completed order.",
"scope": "/state/currentStatus",
"schema": {
"enum": [
"completed"
]
},
"invoke": "https://example-payment-adapter.norce.io/api/v1/callbacks/orders/{order_id}/state-changed"
}
],
"total": {
"includingVat": 200,
"excludingVat": 160
}
}
Example Flow
In this example flow we can see how an event triggered by Klarna results in the Norce adapter reacting using notifications.
- Klarna webhook calls the Klarna adapter to inform it the payment has been accepted.
-
The Klarna adapter updates its payment and changes the order state from
accepted
tocompleted
. -
The order service commits the change and a notification for Norce adapter is queued for processing, since it wished to be inform of state being changed to
completed
. The Klarna adapter receives a response and can in turn accept the webhook from Klarna. - The Notification service processes the queued notification and invokes the Norce adapter.
- The Norce adapter completes the payment in Norce Commerce.