Account deposits
Allaria detects deposits into Allaria+ (wallet) accounts and notifies
subscribed consumers through an outbound webhook. When a transfer is received in
an account owned by the consumer, Allaria sends a POST to the endpoint provided
by the consumer with the payload described below.
Looking for fund deposits into Allaria (brokerage) accounts? Those are notified by a separate webhook — see Listen for fund deposits.
To add new consumers with different filters and notified fields, you must coordinate with the team responsible for the webhook.
The notification
A notification is sent every time a transfer is received in an Allaria+ account
whose owner is the consumer. Transfers to accounts with a different owner do
not trigger the webhook.
Fields that do not apply are still present in the payload, set to null — they
are not omitted. Parse the payload leniently: ignore the fields you do not need,
and do not treat an unknown field as an error, since new ones may be added over
time.
Payload
{
"origin": "ALLARIA+",
"amount": 75000.00,
"cvu": "0000003100010000000001",
"currency": null,
"event_id": "8c41b2d0-55ae-4f39-b7c1-0d9e3a6f2411"
}
| Field | Type | Description |
|---|---|---|
origin | string | Identifies the source of the deposit. Fixed value: "ALLARIA+". |
amount | number | Amount of the received transfer. |
cvu | string | CVU of the target Allaria+ account for the transfer. |
currency | null | Not sent for this scenario. Always null; these transfers are always in pesos. |
event_id | string | UUID identifying the event. Stable across retries — use it to deduplicate. |
This payload carries no target_id: an Allaria+ account is addressed by its CVU.
Trigger summary
| Deposit source | Trigger condition | Identifier in the payload |
|---|---|---|
| Received transfer | Account under the consumer's ownership | cvu (CVU of the target account) |
Webhook signature
Every notification is signed, so you can verify that Allaria sent it and that the body was not altered in transit. This matters because the endpoint that receives these notifications is reachable over the internet: without verifying the signature, anyone who learns its URL can post a deposit that looks legitimate.
HTTPS alone does not cover this — it protects the request in transit and authenticates the server, but says nothing about who sent the request.
Signing is enabled per consumer and coordinated with the team responsible for the webhook: you implement the validation first, we hand over the shared secret through a secure channel, and only then do we start signing your notifications. Until that is agreed, the headers below are not sent.
Headers
Each POST carries two additional headers:
X-Allaria-Timestamp: 1790000000
X-Allaria-Signature: v1=75748751a43a167440a3fdeac209f2ba8c200640027d2234f0d242655ada271c
| Field | Type | Description |
|---|---|---|
X-Allaria-Timestamp | string | Time the request was signed, in seconds since the Unix epoch (UTC). |
X-Allaria-Signature | string | Signature scheme version, then the signature: v1= followed by 64 lowercase hexadecimal characters. |
The v1 prefix versions the scheme. If the algorithm ever changes, a different
prefix will appear — treat an unknown prefix as invalid rather than ignoring it.
How the signature is built
signed_payload = timestamp + "." + body
signature = HMAC_SHA256(signed_payload, secret) → lowercase hex
Where timestamp is the exact string sent in X-Allaria-Timestamp, body is the
raw bytes of the request body as transmitted, and secret is the shared secret
issued to you, in UTF-8. The separator is a single dot.
Validating a request
- Read the raw request body, before any JSON parsing. Do not parse and re-serialize it — see the warning below.
- Reject the request if either header is missing, or if the signature does not
start with
v1=. - Compare
X-Allaria-Timestampagainst your current time and reject the request if the difference is greater than 5 minutes. This is what prevents a captured request from being replayed later. - Recompute the signature and compare it using a constant-time comparison
(
crypto.timingSafeEqual,hmac.compare_digest,MessageDigest.isEqual). - Respond
401when verification fails, and process the notification only when it succeeds.
A failed notification is retried up to three times, and each retry is signed
again with a fresh timestamp and signature. Deduplicate on the payload's
event_id, which is stable across attempts — never on the signature or the
timestamp, which change every time.
Test vector
Use this to confirm your implementation before the secret is exchanged. The body below is arbitrary — the signature is computed over bytes, so any body works the same way.
secret = test_secret_do_not_use_in_production
timestamp = 1790000000
body = {"example":"payload"}
signed_payload = 1790000000.{"example":"payload"}
signature = 75748751a43a167440a3fdeac209f2ba8c200640027d2234f0d242655ada271c
If your code produces that signature from those inputs, it is correct. You can check it from a terminal with:
printf '%s' '1790000000.{"example":"payload"}' \
| openssl dgst -sha256 -hmac 'test_secret_do_not_use_in_production'
Note that a well-implemented receiver must reject a live request carrying this timestamp, because it falls outside the freshness window. That is also a valid test.
Common mistakes
Most failed integrations come from validating the signature against a body that
was parsed and serialized again. JSON.parse + JSON.stringify, or binding the
request straight to a typed object, can change key order, whitespace or decimal
notation, and the resulting signature will never match. Frameworks often consume
the body before your code sees it — in Express, capture it with the verify
option of express.json(); in Spring, receive the body as a String rather than
a DTO.
Also worth checking:
- Do not compare signatures with
==or.equals(). A constant-time comparison avoids leaking the expected value through response timing. - Do not skip the timestamp check. Without it, a captured request stays valid forever.
- Do not trust the timestamp header on its own. It is covered by the
signature, which is why it must be part of
signed_payload— reading it without verifying the signature proves nothing. - Never log the secret, or the signature headers, in access or debug logs.
- A valid signature is authentication, not authorization. It proves the notification is genuinely ours; it does not exempt you from validating amounts, accounts and duplicates.
Recover missed notifications
If your webhook endpoint was unreachable, a notification can end up failed after its retries are exhausted. These two endpoints let you list the notifications that failed and ask Allaria to send them again. Both are authenticated with your access token (see Get started) and only ever act on your own notifications.
+ in originFor Allaria+ the origin value is ALLARIA+. In a query string the + must be
percent-encoded as %2B (origin=ALLARIA%2B); an unencoded + is decoded as a
space and rejected. In a JSON body it is sent literally ("origin": "ALLARIA+").
List failed notifications
/v1/outbound-notifier/notifications/failedReturns the event_ids of the events whose notification to you ended in a failed
state, within a date range.
Request
- cURL
- JavaScript
- Python
- Node.js
curl -X GET "https://b2b.allaria.cloud/v1/outbound-notifier/notifications/failed?origin=ALLARIA%2B&from=2024-01-01&to=2024-01-31" \
-H "Authorization: Bearer ACCESS_TOKEN"
// URLSearchParams encodes the "+" as %2B automatically.
const params = new URLSearchParams({
origin: 'ALLARIA+',
from: '2024-01-01',
to: '2024-01-31',
});
const response = await fetch(`https://b2b.allaria.cloud/v1/outbound-notifier/notifications/failed?${params}`, {
method: 'GET',
headers: { 'Authorization': 'Bearer ACCESS_TOKEN' },
});
const data = await response.json();
console.log(data);
import requests
response = requests.get(
'https://b2b.allaria.cloud/v1/outbound-notifier/notifications/failed',
headers={ 'Authorization': 'Bearer ACCESS_TOKEN' },
params={ 'origin': 'ALLARIA+', 'from': '2024-01-01', 'to': '2024-01-31' },
)
data = response.json()
print(data)
const axios = require('axios');
const response = await axios.get('https://b2b.allaria.cloud/v1/outbound-notifier/notifications/failed', {
headers: { Authorization: 'Bearer ACCESS_TOKEN' },
params: { origin: 'ALLARIA+', from: '2024-01-01', to: '2024-01-31' },
});
console.log(response.data);
Headers
| Field | Type | Required | Description |
|---|---|---|---|
Authorization | string | Yes | Access token in Bearer ACCESS_TOKEN format |
Query Parameters
| Field | Type | Required | Description |
|---|---|---|---|
origin | string | Yes | Source of the events. For this webhook, ALLARIA+ (send it as ALLARIA%2B). |
from | string (date) | No | Start of the range, YYYY-MM-DD (inclusive, Buenos Aires time). Defaults to today. |
to | string (date) | No | End of the range, YYYY-MM-DD (inclusive, Buenos Aires time). Defaults to today. |
Response
2XX Success
Status: 200 OK
{
"event_ids": [
"8c41b2d0-55ae-4f39-b7c1-0d9e3a6f2411",
"a1b2c3d4-e5f6-7890-abcd-ef1234567890"
]
}
| Field | Type | Description |
|---|---|---|
event_ids | array[string (uuid)] | UUIDs of the events whose notification to you ended in a failed state. Same event_id as in the webhook payload. Empty when there are none. |
Error Codes
| Status | Description |
|---|---|
400 | Bad Request — missing or invalid origin, or from is after to |
401 | Unauthorized — invalid or missing access token |
403 | Forbidden — the token is not authorized for this webhook |
Reprocess notifications
/v1/outbound-notifier/notifications/reprocessQueues the failed notifications of the given events to be sent again. Reprocessing
is asynchronous: a 202 means the request was accepted, not that delivery has
finished. Each queued notification is retried like the original (up to three
attempts) and the resend reaches your webhook endpoint signed like any other
notification — deduplicate by event_id.
Request
- cURL
- JavaScript
- Python
- Node.js
curl -X POST "https://b2b.allaria.cloud/v1/outbound-notifier/notifications/reprocess" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"origin": "ALLARIA+",
"event_ids": [
"8c41b2d0-55ae-4f39-b7c1-0d9e3a6f2411",
"a1b2c3d4-e5f6-7890-abcd-ef1234567890"
]
}'
const response = await fetch('https://b2b.allaria.cloud/v1/outbound-notifier/notifications/reprocess', {
method: 'POST',
headers: {
'Authorization': 'Bearer ACCESS_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({
origin: 'ALLARIA+',
event_ids: [
'8c41b2d0-55ae-4f39-b7c1-0d9e3a6f2411',
'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
],
}),
});
const data = await response.json();
console.log(data);
import requests
response = requests.post(
'https://b2b.allaria.cloud/v1/outbound-notifier/notifications/reprocess',
headers={
'Authorization': 'Bearer ACCESS_TOKEN',
'Content-Type': 'application/json',
},
json={
'origin': 'ALLARIA+',
'event_ids': [
'8c41b2d0-55ae-4f39-b7c1-0d9e3a6f2411',
'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
],
},
)
data = response.json()
print(data)
const axios = require('axios');
const response = await axios.post(
'https://b2b.allaria.cloud/v1/outbound-notifier/notifications/reprocess',
{
origin: 'ALLARIA+',
event_ids: [
'8c41b2d0-55ae-4f39-b7c1-0d9e3a6f2411',
'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
],
},
{
headers: {
Authorization: 'Bearer ACCESS_TOKEN',
'Content-Type': 'application/json',
},
}
);
console.log(response.data);
Headers
| Field | Type | Required | Description |
|---|---|---|---|
Authorization | string | Yes | Access token in Bearer ACCESS_TOKEN format |
Content-Type | string | Yes | Must be application/json |
Body
| Field | Type | Required | Description |
|---|---|---|---|
origin | string | Yes | Source of the events. For this webhook, ALLARIA+ (sent literally in the JSON body). |
event_ids | array[string (uuid)] | Yes | Up to 100 event UUIDs to reprocess. Duplicates are ignored. |
Response
2XX Success
Status: 202 Accepted
{
"reprocessed": [
"8c41b2d0-55ae-4f39-b7c1-0d9e3a6f2411"
],
"rejected": [
{
"event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"reason": "NOT_FAILED"
}
]
}
| Field | Type | Description |
|---|---|---|
reprocessed | array[string (uuid)] | Events whose failed notification was queued to be sent again. |
rejected | array[object] | Events that were not reprocessed, each with a reason. |
rejected[].event_id | string (uuid) | The rejected event. |
rejected[].reason | string | NOT_FOUND (not a failed event of yours) or NOT_FAILED (the notification is not in a failed state). |
Error Codes
| Status | Description |
|---|---|
400 | Bad Request — invalid origin, empty or missing event_ids, a malformed UUID, or more than 100 ids |
401 | Unauthorized — invalid or missing access token |
403 | Forbidden — the token is not authorized for this webhook |
409 | Conflict — notifications to your integration are disabled |