Webhooks

Receive near real-time notifications of the events happening in your Soldo account.

Webhooks let your application react to what happens in your Soldo account without polling the API. When an event you subscribed to occurs (e.g. a card is activated, a transaction is settled or a user is created), Soldo sends an HTTP POST request with a JSON payload describing the event to a URL you control.

Many events are not the result of an API request (e.g. a cardholder paying with their card or an admin editing a wallet from the web console), so webhooks are the only way to be notified about them as soon as they happen.

How it works

  1. You expose an HTTPS endpoint that meets the requirements.
  2. You create a webhook subscription, specifying your endpoint URL and the list of events you want to receive.
  3. When one of those events occurs, Soldo sends a notification to your endpoint. The notification includes three X-Soldo-Fingerprint* headers you use to verify that it was really sent by Soldo.
  4. Your endpoint replies with a 2xx HTTP status code to acknowledge receipt. Otherwise, Soldo retries the delivery.

Before you begin

📘

Prerequisites

  • You've set up the API and you can complete both standard and advanced authentication.
  • Your API credentials have the webhook_subscription_read and webhook_subscription_write scopes.
  • You have your fingerprint token, provided together with your client_id and client_secret. You'll need it to verify the notifications you receive.
  • You have the Soldo public key for the environment you're integrating with.

Your endpoint must:

  • be reachable from the internet over HTTPS;
  • accept POST requests with a JSON body (Content-Type: application/json).

Subscribe to events

To start receiving notifications, add a webhook subscription with the URL of your endpoint and the list of events you want to receive. Subscribing requires advanced authentication. You can find the fingerprint order and the full list of request fields on the endpoint's reference page.

curl --request POST \
     --url https://api.soldo.com/business/v2/webhooks/subscriptions \
     --header 'accept: application/json' \
     --header 'authorization: Bearer {access_token}' \
     --header 'content-type: application/json' \
     --header 'X-Soldo-Fingerprint: {fingerprint}' \
     --header 'X-Soldo-Fingerprint-Signature: {fingerprint_signature}' \
     --data '{
       "webhook_url": "https://yourdomain.com/soldo/webhooks",
       "enabled_events": ["card_activated", "joule_feed", "new_user"],
       "description": "Cards, settlements and new users",
       "active": true
     }'

The response contains the new WebhookSubscription, including its id. The same id is included as subscription_id in every notification delivered through this subscription, so you can tell which subscription routed an event.

👍

Tip

Create the subscription with "active": false first, then activate it with Update Webhook Subscription once your endpoint is deployed and able to verify notifications.

Available events

The full list of events is in the Webhooks section of the API reference. The reference page of each event describes:

  • the event name, to use in the enabled_events field of your subscription;
  • the event_type and the full schema of the data object, with an example payload;
  • the fingerprint order used to calculate the X-Soldo-Fingerprint header.

Manage your subscriptions

You can create several subscriptions, for example to route different events to different systems. Use Update Webhook Subscription to change the URL, add or remove events, or temporarily pause notifications by setting active to false. All the endpoints to search, retrieve, update and delete your subscriptions are in the Webhook Subscriptions section of the API reference.

Receive a notification

Each notification is an HTTP POST request sent to your webhook_url:

POST /soldo/webhooks HTTP/1.1
Host: yourdomain.com
Content-Type: application/json
X-Soldo-Fingerprint-Order: id, name, status, walletId
X-Soldo-Fingerprint: {fingerprint}
X-Soldo-Fingerprint-Signature: {fingerprint_signature}

{
  "event_uuid": "7bf79328-a7ae-4222-aa54-091ff7f429a7",
  "event_type": "Card",
  "event_name": "card_activated",
  "company_account_id": "XMPL1234",
  "subscription_id": "2d65bd5e-3fdf-4002-b166-bde7fb8863fa",
  "data": {
    "id": "3ada77cf-cae3-4a23-98c3-ccbd0ab6e03b",
    "name": "Marketing card",
    "status": "Normal",
    "wallet_id": "0ef8b5d6-0c5e-4f2e-a1b1-6a7c2a9d1e11",
    ...
  }
}

Every notification has the same envelope: an event_uuid that identifies the event, its event_type and event_name, the company_account_id and the subscription_id that routed it, and a data object containing the affected resource. The three X-Soldo-Fingerprint* headers are used to verify the notification.

The full description of headers and fields, and the schema of data, are on each event's reference page in the Webhooks section of the API reference.

Verify the notification

Your endpoint is publicly reachable, so anyone could send it a request that looks like a Soldo notification. Always verify a notification before acting on it. Verification has two steps, and both must pass:

  1. Verify the signature. Check that X-Soldo-Fingerprint-Signature, once Base64-decoded, is a valid SHA512withRSA signature of X-Soldo-Fingerprint, using the Soldo public key. This proves the fingerprint was produced by Soldo.
  2. Verify the fingerprint. Recalculate the fingerprint from the payload you received and check that it matches X-Soldo-Fingerprint. This confirms that the key fields of the notification, listed in X-Soldo-Fingerprint-Order, match the ones signed by Soldo.
🚧

Don't skip step 2

A valid signature on its own only proves that Soldo produced a fingerprint. Recalculating the fingerprint from the payload is what ties that signature to the notification you received.

Recalculate the fingerprint

  1. Read the list of fields from the X-Soldo-Fingerprint-Order header and split it on commas, trimming whitespace.
  2. For each field, in order, take the value of the corresponding attribute of the data object. Field names in the fingerprint order use camelCase, while payload attributes use snake_case (e.g. walletId → data.wallet_id). If the attribute is missing or null, use an empty string.
  3. Concatenate all the values into a single string, with no separator, then append your fingerprint token at the end. The token is always appended, even though it's not listed in X-Soldo-Fingerprint-Order.
  4. Calculate the SHA-512 hash of the string and encode it as lowercase hexadecimal.
  5. Compare the result with X-Soldo-Fingerprint using a constant-time comparison.

For example, given the card_activated notification above, with fingerprint order id, name, status, walletId:

OrderFieldSourceValue
1iddata.id3ada77cf-cae3-4a23-98c3-ccbd0ab6e03b
2namedata.nameMarketing card
3statusdata.statusNormal
4walletIddata.wallet_id0ef8b5d6-0c5e-4f2e-a1b1-6a7c2a9d1e11
5tokenfingerprint tokenQDX6RK6OH1CZLSWOUWBA

The concatenated string is 3ada77cf-cae3-4a23-98c3-ccbd0ab6e03bMarketing cardNormal0ef8b5d6-0c5e-4f2e-a1b1-6a7c2a9d1e11QDX6RK6OH1CZLSWOUWBA, and its SHA-512 hash must be equal to the value of X-Soldo-Fingerprint.

📘

Fingerprint token

QDX6RK6OH1CZLSWOUWBA is a sample value. Replace it with the fingerprint token you received while setting up the API.

📘

Use the header, not a hard-coded list

The fingerprint order of each event is documented on its reference page, but always build the fingerprint from the X-Soldo-Fingerprint-Order header you receive. This way your code keeps working if the order changes or new events are added.

Code sample

The example below uses Python and the cryptography package. A Java implementation of the signature check (checkSignatureByPublicKey) is available in the Java code sample.

import base64
import hashlib
import hmac
import re

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding

FINGERPRINT_TOKEN = "YOUR_FINGERPRINT_TOKEN"  # load it from a secret store, never hard-code it
with open("soldo_public_key.pem", "rb") as f:
    SOLDO_PUBLIC_KEY = serialization.load_pem_public_key(f.read())


def to_snake_case(name):
    # walletId -> wallet_id
    return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()


def field_value(data, field):
    value = data.get(field, data.get(to_snake_case(field)))
    if value is None:
        return ""
    if isinstance(value, bool):
        return str(value).lower()
    return str(value)


def is_authentic(headers, event):
    fingerprint = headers["X-Soldo-Fingerprint"]
    signature = headers["X-Soldo-Fingerprint-Signature"]
    fields = [f.strip() for f in headers["X-Soldo-Fingerprint-Order"].split(",")]

    # 1. The signature proves the fingerprint was produced by Soldo
    try:
        SOLDO_PUBLIC_KEY.verify(
            base64.b64decode(signature),
            fingerprint.encode("utf-8"),
            padding.PKCS1v15(),
            hashes.SHA512(),
        )
    except (InvalidSignature, ValueError):
        return False

    # 2. The fingerprint ties the signature to this notification's key fields
    concatenated = "".join(field_value(event["data"], f) for f in fields) + FINGERPRINT_TOKEN
    expected = hashlib.sha512(concatenated.encode("utf-8")).hexdigest()
    return hmac.compare_digest(expected, fingerprint)
⚠️

Header names are case-insensitive

Many frameworks normalise header names (e.g. x-soldo-fingerprint). Read them through your framework's case-insensitive accessor.

⚠️

Keep decimal numbers as they are

Some fingerprint fields are decimal numbers (e.g. amounts), and their value is hashed exactly as it appears in the payload, trailing zeros included: 100.50 is hashed as 100.50, not 100.5. Many JSON parsers convert decimals to floating-point numbers and lose the trailing zeros, which makes the fingerprint check fail. In Python, parse the request body with json.loads(body, parse_float=Decimal) (with from decimal import Decimal) before passing it to is_authentic. In other languages, use a decimal type or read the raw number string.

Soldo public key

The Soldo public key is a 2048-bit RSA key in X.509 SubjectPublicKeyInfo format (OpenSSL PEM). Use the key matching the environment the notification comes from. Save it to a file (e.g. soldo_public_key.pem) to use it with the code sample.

Sandbox (https://api-demo.soldocloud.net)

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAomgGIMuKXygxmqFc2jgi
EsJMK/Tqd8n6wflHI+zOu4u+ewp1W+xp67Ns1sBWg3Po3kZFErZud4T9XygW+VS/
doZ8R4TD1tWSpyudpYM4qptDrTc7R2XRw4x+1NN1fgwciG2zbAgs3OD0KtMMq7TC
KCBYwDLyYuiLd488Amcrpqob4270tTtPVL4vXJoPiMs4cymZfuxy8RJboLzPLbdt
e2JFt//sVJaa8AHmNYKXbRL3ZxCwozGZiqpDhPCSFX+c6UN+wpg88es3EpB/cppr
g2EGBfOJSYtWaFjBGbQDGG6iE/ayaAhS6zRPyjTwfZdM+JGTeAPCBgN9eTP2ffI0
DwIDAQAB
-----END PUBLIC KEY-----

Production (https://api.soldo.com)

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAj+O9gb/79gsOFb1TIwJN
dbr5v4sKcexw5YO9z9ROZJuR6eCvZeI3/wVQvWIRUj9CpL6DhtveuCIy6ZDPEGd5
2v1J8RkGDizvN1uBPYLZkDxkqXQuFdx5e+Toh0G1ohWpSVsMkgEe6XtELXtllVjl
kzch3d2O3wCECFaZmIa60kV9qkJsewKa5SRDvvA2Y9PxBiVVwc7VHNwMX4Xo7aA1
tCBOKVyL9OSehA4vjESOpLGpg4A0M7dYgBBMaGiIGZuSmtudcdfv+pRMT63HWEgx
GV/8oaVLO0nfmhnguNqRhZy/Kf7Kc9ITvws+tmTodS49kqiEBUK1fqIwLmh7TELi
BwIDAQAB
-----END PUBLIC KEY-----
📘

Not the same as your public key

The Soldo public key is used to verify notifications sent by Soldo. It's different from the public key you uploaded when setting up the API, which Soldo uses to verify the requests you send.

Respond to the notification

Reply with any 2xx status code (e.g. 200 OK or 204 No Content) once you've verified the notification and stored it, before running any long-running processing. The response body is ignored.

Retries

If your endpoint replies with a status code other than 2xx, times out or can't be reached, Soldo retries the delivery with an exponential back-off: the interval between attempts grows from a few minutes up to 24 hours. Retries continue for about 2.5 days, after which the notification is no longer delivered. See Best practices to recover missed notifications.

❗️

No retry on 400 Bad Request

If your endpoint replies with 400 Bad Request, the notification is considered rejected and is never retried. If your endpoint fails because of a temporary problem on your side (e.g. your database is unavailable), reply with a 5xx status code instead, so that Soldo retries the delivery.

While your endpoint is unavailable, Soldo may pause the delivery of new notifications until it's reachable again. When delivery resumes, you may receive several notifications in a short time.

Best practices

  • Handle duplicates. The same event may be delivered more than once (e.g. when a retry follows a response that was lost on the network). Store the event_uuid of processed events and skip the ones you've already handled.
  • Use the API as the source of truth. Notifications can arrive in a different order from the one in which the events occurred, and the fingerprint covers only the key fields of each event. Before acting on information that matters to your business (e.g. amounts or statuses), retrieve the resource from the API (e.g. Get Card using data.id).
  • Reconcile periodically. Some notifications may never reach your endpoint (e.g. after a 400 Bad Request reply, or if delivery keeps failing for the whole retry period). Keep track of when you last processed a notification and, after an outage or periodically as a safety net, use the search endpoints of the resources you care about to retrieve what changed since then.
  • Process asynchronously. Put verified notifications on a queue and process them in the background, so your endpoint replies quickly and doesn't trigger retries.
  • Subscribe only to what you need. Fewer events means less traffic to your endpoint and less data to protect.
  • Keep your fingerprint token secret. It's never sent in API requests or notifications. If you think it has been compromised, contact business support.
  • Protect the data you receive. Notifications can contain personal data (e.g. users' names and email addresses) and masked card details. Only log what you need, and apply the same security and data-retention controls you use for data retrieved from the API.
  • Monitor your endpoint. Alert on non-2xx responses and verification failures, so you can fix issues within the retry window.

Troubleshooting

SymptomWhat to check
No notifications are receivedThe subscription is active and its enabled_events contain the event you expect (Get Webhook Subscription). Your endpoint is reachable from the internet over HTTPS and isn't blocked by a firewall.
The same notification is received many timesYour endpoint replies with a 2xx status code, and replies quickly enough. Any other response, except 400, causes a retry.
Notifications stopped, then arrived in bulkYour endpoint was unavailable (e.g. it timed out, couldn't be reached or replied with a 5xx status code), so delivery was paused until it was reachable again. See Retries.
A notification was never received againYour endpoint replied with 400 Bad Request, which is never retried, or the delivery kept failing for the whole retry period. See Best practices to recover missed notifications.
Signature verification failsYou're using the Soldo public key for the right environment (sandbox or production), not the public key you uploaded to Soldo.
Fingerprint verification failsYou're building the fingerprint from the X-Soldo-Fingerprint-Order header you received, not from a hard-coded list of fields, and you're appending your fingerprint token at the end. See Recalculate the fingerprint.

Did this page help you?