Treasury¶
In this chapter we will show examples of making a forex trade, and of reading the balances of the bank accounts your organization has authorized.
Schedule Currency Trading¶
Create a new trade by using the scheduleCurrencyTrading mutation. This mutation will create a new trade in the 360t system.
For details of what each input field means, you can check ScheduleCurrencyTradingInput.
The waitToken in the response can be used to check for the status of the trade.
By using the fxForward query, you can get the status and execution details.
Webhook Notifications¶
Kronor will send notifications to the webhook URL provided by the merchant. The notification will be sent when the trade status changes.
The notification will be a POST request with the following payload:
{
"events": [
{
"event": "fxForwardUpdateEvent",
"id": "123456",
"triggeredAt": "2024-11-11T14:42:45.315318+00:00",
"additionalData": [{
"amount": 100000000,
"createdAt": "2024-11-11T14:42:45.315318+00:00",
"currencyToBuy": "EUR",
"currencyToPayWith": "USD",
"desiredQuotes": 4,
"errorMessage": null,
"fxForwardExecutionDetail": {
"executedQuoteId": "2-000004",
"executedRate": "1.05031",
"spotRate": "1.05031",
"forwardPoints": null,
"executionId": "457021057",
"id": 2,
"orderId": "2",
"settleDate": "2024-11-18"
}],
"nominalCurrency": "EUR",
"settlementDate": "2024-11-18",
"status": "success",
"id": 12345
}
}
]
}
Account Balances¶
Once your organization has authorized Kronor to access its bank accounts, we fetch the balances of those accounts and send the result to your organization’s webhook endpoint. Balances come from Danske Bank.
Two events report on it:
Event |
Meaning |
|---|---|
|
Balances were fetched and stored for one account. |
|
A balance did not arrive. |
Three things have to be true before an account produces either of them. Danske
Bank has to be the account’s balance provider, balance fetching has to be
switched on for that account, and Danske has to still be listing the account to
us. Balance fetching is off by default and is switched on per account, so
authorizing an account is not by itself enough. These events are Danske only:
an account whose balances come from another provider sends neither, and
provider is always DANSKE_BANK.
On top of that both events are disabled by default for your organization, so nothing is sent until you enable them.
Enabling the balance events¶
These are organization webhooks, which are separate from the merchant events in Webhook Notifications: turning on one does not turn on the other, and they are registered in a different place.
That place is the merchant portal, not your backend. The mutation behind it,
setOrganizationWebhookUrl, is not part of the schema an API token sees, so
someone with portal access does this rather than your integration. Two pages set
it up:
Page |
What it does |
|---|---|
Register the URL that receives the events and tick |
|
Tick the authorized accounts whose balances should be fetched. Every account starts unticked, so an endpoint with the event enabled and no account ticked here receives nothing. |
The two pages under Bank Accounts need a portal administrator; Webhooks does not.
Once both are set, Bank Accounts → Accounts can
refresh a single account on demand. That fetches immediately and sends a real
aisBalance, which is the way to check your endpoint without waiting for the
next scheduled run.
The signing secret is shown once, when the endpoint is created. Copy it then: it is not displayed again afterwards, and an endpoint whose secret has been lost has to be replaced with a new one.
Events are enabled per endpoint, so ticking aisBalance on one endpoint does
not affect another, and unticking it is what stops delivery. An organization can
have several endpoints, up to five active ones. Every endpoint with the event
enabled gets its own copy, so two endpoints means two deliveries of the same
event, each signed with its own secret. Deleting an endpoint discards whatever
was still queued for it.
Enabling an event is not retroactive. Only events that happen after you enable it are queued, and there is no backfill of balances fetched before that.
Delivery¶
The endpoint works the same way as it does for merchant webhooks: a JSON
POST, an X-HMAC-SHA256-Signature header covering the exact raw request
body, and a reply of HTTP 200 with [accepted] within 10 seconds. Anything
else counts as a failure and the batch is retried. The retry schedule is the one
in Queued notifications,
and an endpoint that keeps failing for about seven days is switched off and has
to be set up again.
Verify the signature on every request before you act on it. The payload carries account names, IBANs and balances, and an endpoint that does not check the signature will accept a balance from anyone who learns the URL. The worked example in Expose an endpoint on your server applies here as well. Compare the digests with a constant-time comparison, and verify against the bytes as received, before parsing or re-serialising the JSON.
Up to ten events are sent in one request, oldest first, so an event per account
does not mean an HTTP request per account. A batch is not one kind of event: an
aisBalance can arrive in the same request as a settlementReportReady, so
dispatch on each event’s event field rather than on the endpoint it came to.
{
"events": [
{
"event": "aisBalance",
"id": "72316",
"triggeredAt": "2026-09-15 07:30:12.481273+00",
"additionalData": { }
}
]
}
id is a decimal number sent as a string. triggeredAt is a Postgres
timestamp, always UTC, written with a space instead of a T and an offset of
+00, so a strict RFC 3339 parser will reject it. Read it with a format that
accepts a space, or normalise it before parsing.
A batch is accepted or rejected as a whole, so a retry can redeliver events you have already handled.
aisBalance¶
Sent once per account rather than once per balance. Every balance the bank returned for an account arrives in the same event.
{
"events": [
{
"event": "aisBalance",
"id": "72316",
"triggeredAt": "2026-09-15 07:30:12.481273+00",
"additionalData": {
"accountId": 40721,
"provider": "DANSKE_BANK",
"balances": [
{
"type": "ITBD",
"sourceType": "interimBooked",
"amount": "482915.30",
"amountMinor": 48291530,
"exponent": 2,
"currency": "DKK",
"referenceDate": "2026-09-15T04:31:07.412993Z"
},
{
"type": "IGBD",
"sourceType": "intraGroupBooked",
"amount": "73204.88",
"amountMinor": 7320488,
"exponent": 2,
"currency": "DKK",
"referenceDate": "2026-09-15T04:31:07.412993Z"
}
],
"accountInfo": {
"name": "EXAMPLE TRADING A/S",
"details": "Business account",
"iban": "DK8030000012345678",
"bban": "30000012345678"
}
}
}
]
}
Field |
Description |
|---|---|
accountId |
Identifies the account the balances belong to. This is the same identifier
as in the |
provider |
Which provider the balances came from. Always |
balances[].amount |
The balance in major units, as a decimal string, exactly as the bank
reported it. Parse it as a decimal, or use |
balances[].amountMinor |
The same figure as a signed integer in minor units. This is the value Kronor stores, so it will match the figure in a Kronor report exactly. |
balances[].exponent |
The scale |
balances[].currency |
ISO 4217 alpha-3 currency code. |
balances[].type |
Normalised balance type, see Balance types. |
balances[].sourceType |
The bank’s own name for the balance, unnormalised. Useful when you need a
distinction that |
balances[].referenceDate |
When the bank says the balance applies. Passed through as the bank sent
it, so treat the format as the bank’s rather than ours. Can be |
accountInfo |
Descriptive fields for the account. Any of |
Unlike the merchant webhook events, where all money is in minor units,
amount here is in major units. Use amountMinor if you want minor units.
Balances can be negative, and the sign is carried in both amount and
amountMinor. There is no separate debit indicator. Accounts in a cash pool
often hold large negative positions, so a negative balance is normal rather than
an error.
accountInfo carries the account holder’s name and account numbers. Treat the
event as personal data when you decide how long to keep raw payloads and who can
read them.
A balance the bank returns that we cannot convert to an exact integer amount is
left out of balances and recorded on our side instead. The array is
therefore what we could represent, which is not always everything the bank
returned.
aisBalanceFetchFailed¶
Sent when a balance did not arrive. There are three reasons, and kind tells
them apart.
{
"events": [
{
"event": "aisBalanceFetchFailed",
"id": "72319",
"triggeredAt": "2026-09-15 08:12:44.019822+00",
"additionalData": {
"accountId": 40721,
"provider": "DANSKE_BANK",
"kind": "REFUSED",
"reason": "Danske will not answer for this account; it has to be reconnected before balances resume"
}
}
]
}
|
What happened |
How often |
|---|---|---|
|
A fetch ran and did not update the balance. The bank was unavailable, or the job did not get to run in time. Your figure is one cycle older than it should be. The next run usually fixes it. |
Every run that fails. |
|
The bank will not answer for this account. It will not fix itself. The authorization has to be reconnected before balances start again. |
Once, until balances arrive again. |
|
No balance has been stored for this account in six hours. |
Once, until balances arrive again. |
Branch on kind. reason is a sentence for whoever is reading the logs. It
is not a fixed set of values and the wording will change.
FETCH_FAILED arrives once for every run that fails. REFUSED and
STALE arrive once. You get another only after balances have started arriving
again, so an account that stays refused does not send one every hour. An account
already reported as STALE can still send a REFUSED later.
STALE is the one to alarm on. It covers every reason an account stops
reporting, including the ones that send nothing else.
Balance types¶
|
|
Meaning |
|---|---|---|
|
|
Booked balance of the account itself. |
|
|
Value dated balance. |
|
|
Booked position in the group’s cash pool. |
|
|
Intra-group interest position. |
|
anything else |
A balance type we do not normalise yet. |
Danske names these four values but does not define them, so the meanings above
are our reading of them and VALU in particular is inferred. sourceType
always carries the bank’s own name, so use that if you need to be sure of what
you are looking at.
Only the types the bank actually returned are present, and which ones those are
differs between accounts. An account in a cash pool usually reports ITBD as
0.00, because the money is swept into the pool and reported under IGBD,
while a standalone account has no IGBD at all. Do not expect a fixed set of
types, and do not treat a missing type as zero.
When nothing is sent¶
Some things still send nothing:
An account we have never held a balance for. A closed account, or one your technical user cannot query, counts as empty, not stale.
Balance fetching is switched off for the account, or the events are disabled for your organization.
An account whose balances come from a provider other than Danske Bank.
STALE covers an account that used to report and stopped. It does not cover
one that never reported. If nothing has ever arrived, check the two switches in
Enabling the balance events. The account has to be chosen on the balance
fetch accounts page, and the events have to be ticked on the endpoint.
Ordering events¶
An aisBalance event is sent after every successful fetch, whether or not the
balance moved since the last one, so most events repeat the figures from the
previous one. When you have two events for the same account, order them by
triggeredAt. It is set when we store the balance, in the same transaction,
so it is the time the figure was read rather than the time the request happened
to reach you. Events are queued and sent oldest first as well.
A manual refresh from the merchant portal fetches immediately and sends an
aisBalance the same way, so events do not only arrive on a schedule.
referenceDate answers a different question, which is what moment the figure
describes according to the bank. It is the right thing to show next to a
balance, but not to sort by: it can be null, and it stays the same across
fetches while the balance has not moved.