Callbacks
Pliant uses HTTPS callbacks (also known as webhooks) to notify partners about specific events that occur within our platform.
Partners can subscribe to these callbacks via the API. Once subscribed, your system will be notified whenever the specified event type occurs.
Callback Structure
- Callbacks events are sent as HTTPS
POSTrequests. - Each callback includes a structured
bodycontaining event-specific information. - Callbacks can be:
- Unauthenticated, or
- Authenticated using OAuth 2.0 (see Authenticated Callback Usage).
- Callback authenticity can be verified, see below
Each callback request contains the following HTTP headers to assist with identification and processing:
| Header | Description | Example Value |
|---|---|---|
X-Organization-Id | The organization associated with the event. | 54e6e7e7-0c23-4a54-bccb-2d3ff2fd02df |
X-Partner-Organization-Id | (Optional) The partner’s reference to the organization. | abcdf1234 |
X-Payment-Program-Id | The payment program related to the event. | 54e6e7e7-0c23-4a54-bccb-2d3ff2fd02df |
X-Callback-Id | Unique UUIDv4 for the callback. Useful for deduplication. | 12a03093-eaad-4ae5-9d8d-42e7f6f8b7c6 |
X-Callback-Created-At | UTC timestamp of when the callback was created. May be used to re-order delayed callbacks. | 2024-03-15T15:55:58.623375258Z |
webbhook-* | Three headers for callback verification | See below |
Callback Semantics
We only send the callback. It's up to you to ensure successful processing on your side.
Callbacks are delivered with at least once semantics. This means:
- A callback may be sent more than once for the same event.
- While we attempt to deduplicate events, due to the asynchronous and distributed nature of the system, duplicates cannot be fully avoided.
- You should implement idempotent endpoints to safely handle repeated deliveries.
95% of callbacks are delivered within 10 seconds of the business event. The remaining 5% may take up to 1–2 minutes, depending on system load and event sequencing.
Retry Strategy
If your system responds with a non-2xx HTTP status code, Pliant will retry the callback automatically:
- We do not follow redirects (e.g.,
301,302). - Retry pattern:
4xxerror codes are retried around five times- all other error codes are retried around twenty times, in a pattern with growing times between each retry
- Based on the error code, the callback can be marked as failed (either after few retries or twenty).
- You can request a manual retry by providing the callback ID(s) and the desired timeframe.
Important Notes
Callbacks reflect events in a fully asynchronous system. This means:
- Order is not guaranteed — callbacks may arrive out of sequence.
- Timing is not deterministic — most callbacks are fast, but some may be delayed depending on load and processing time.
- Always use the
X-Callback-Created-Attimestamp if you need to reconstruct event order on your end.
Authentication
For information on securing your callback endpoints, please refer to the guide on Authenticated Callback usage.
Callback Authenticity
All callbacks sent by Pliant are cryptographically signed so that consumers can verify that they originate from our systems. The signing process follows the Standard Webhooks Signature Scheme.
Each callback attempt includes three HTTP headers:
-
webhook-id: Identical to the historical headerX-Callback-Id. -
webhook-timestamp: Unix timestamp (seconds since epoch) indicating when this callback attempt was generated. -
webhook-signature: A versioned Ed25519 signature over the callback data.Format:
v1a,<base64-encoded-signature>. Thev1aprefix identifies the signature scheme version.
Both "callback" and "webhook" terminology appear due to compatibility with the Standard Webhooks specification.
Signature Scheme
Each payment program has its own Ed25519 key pair used to sign callbacks. You can retrieve your program’s public key from: /api/partner-management/signing/jwks.json
The endpoint requires authentication using your API credentials.
Signed content format
The signature is computed over the following three components, concatenated verbatim and separated by dots:
<webhook-id>.<webhook-timestamp>.<payload>
Rules for concatenation and serialization:
-
<payload>is exactly the raw HTTP request body, byte-for-byte, including whitespace and ordering. Do not reformat or re-serialize JSON. -
Use UTF-8 encoding for all text components.
-
No trailing spaces or newlines are added or removed.
-
<webhook-timestamp>must be included as an integer string, exactly as received.
Verifying a Callback
To verify a callback:
-
Validate timestamp tolerance
Compare
webhook-timestampto your system time.We recommend enforcing an allowable skew to mitigate replay attacks—for example ±5 minutes. Select a window that fits your operational requirements.
-
Reconstruct the signed content
Concatenate the values exactly as defined.
-
Verify the signature
- Extract the Base64-encoded portion of
webhook-signature(after thev1a,prefix). - Use the Ed25519 public key from the JWKS to verify the detached signature against the reconstructed content.
- Extract the Base64-encoded portion of
Example
A callback may be delivered with headers and payload like the following:
webhook-id: fcc8b37b-9f9a-4e2c-bd0d-4e0610d92ec5
webhook-timestamp: 123456789
webhook-signature: v1a,t6CRz6htNVgx9O1y4PjSeBFZRlhu4fk0fZJy8pYEkgSp4hiOaowWLLzJM737t3jTZNlcw/Tc+m/8tGxm95qsAw==
{"test": true}Signature verification in Kotlin
val jwks = JWKSet.parse("""{"keys":[{"kty":"OKP","use":"sig","crv":"Ed25519","kid":"x4_cic8wT7GiHGhLR7OA5VAC6U3Dn5MDWNTDlCk_Qe4","key_ops":["verify"],"x":"ybZX6AKkLQ2fPIUb_RelEpB7gThMVtuPiDn5upltFxI","alg":"EdDSA"}]}""")
val spki = SubjectPublicKeyInfo(
AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519),
jwks.keys.first().toOctetKeyPair().x.decode(),
)
val publicKey = KeyFactory.getInstance(Curve.Ed25519.stdName).generatePublic(X509EncodedKeySpec(spki.encoded))
val payload = """{"test": true}""" // raw HTTP body
val timestamp = 123456789L
val callbackId = "fcc8b37b-9f9a-4e2c-bd0d-4e0610d92ec5"
val signature = "t6CRz6htNVgx9O1y4PjSeBFZRlhu4fk0fZJy8pYEkgSp4hiOaowWLLzJM737t3jTZNlcw/Tc+m/8tGxm95qsAw=="
val toVerify = "$callbackId.$timestamp.$payload".toByteArray(StandardCharsets.UTF_8)
val sig = Signature.getInstance(Curve.Ed25519.stdName)
sig.initVerify(publicKey)
sig.update(toVerify)
val result = sig.verify(Base64.getDecoder().decode(signature))
assertThat(result).isTrue()Updated 8 days ago
