Get notified about payment updates

A Webhook is a notification sent from Primer to your server. Webhooks will notify you of:

  • payment status updates
    This is especially useful for asynchronous processor Connections, which do not respond with an upfront authorization.
  • opened disputes/chargebacks
    Be notified of disputes across all processors in a unified way.
  • completed refunds
    Primer notifies you when a refund request has been fully processed by a payment processor and the refund has reached a final state.


Setting up webhooks

Set up a Webhook in the Developers area of the Dashboard. Webhooks are sent with a POST request to your designated endpoint.

add a webhook

Failed notification attempts are retried up to three times with a five-second interval.

Testing webhooks

Click the Test webhook link to test your Webhook endpoint.

test a webhook

This will send a POST request to your destination with the example payload shown below. Any response outside the 2XX range, including 3XX HTTP redirection codes, will result in a failure.

{    "message": "Testing your webhook connection"}

Webhook types

Status updates

Payment status notifications are sent when a payment's status has been updated. The webhook body contains the full payment object.

Example payment status notification:

{    "eventType": "PAYMENT.STATUS",    "date": "2023-02-21T15:36:16.367687",    "notificationConfig": {        "id": "cc51f9f0-7e1c-492b-9d37-f83a818f6070",        "description": "Payment webhook"    },    "version": "2.1",    "payment": {        "id": "DdRZ6YY0",        "date": "2023-02-21T15:36:16.167687",        "dateUpdated": "2023-02-21T15:36:16.267687",        "amount": 3000,        "currencyCode": "GBP",        "customerId": "cust-123",        "orderId": "order-123",        "status": "SETTLED",        "paymentMethod": {            "paymentMethodToken": "-lcWjvBAAs2DnIRXwxNjUzNTYzNjIy",            "analyticsId": "LUi5pETUaVsdSEamK25L",            "paymentMethodType": "PAYMENT_CARD",            "paymentMethodData": {                "last4Digits": "1111",                "expirationMonth": "03",                "expirationYear": "2030",                "cardholderName": "ADYEN",                "network": "Visa",                "isNetworkTokenized": false,                "binData": {                    "network": "VISA",                    "issuerCountryCode": "US",                    "issuerName": "JPMORGAN CHASE BANK, N.A.",                    "regionalRestriction": "UNKNOWN",                    "accountNumberType": "UNKNOWN",                    "accountFundingType": "UNKNOWN",                    "prepaidReloadableIndicator": "NOT_APPLICABLE",                    "productUsageType": "UNKNOWN",                    "productCode": "UNKNOWN",                    "productName": "UNKNOWN"                },                "cvvAvailable": true            },            "threeDSecureAuthentication": {                "responseCode": "NOT_PERFORMED"            }        },        "processor": {            "name": "STRIPE",            "processorMerchantId": "acct_1GORasdasqNWFwi8c",            "amountCaptured": 3000,            "amountRefunded": 0        },        "transactions": [            {                "date": "2023-02-21T15:36:16.167687",                "amount": 3000,                "currencyCode": "GBP",                "transactionType": "SALE",                "processorTransactionId": "pi_3L3edsGZasdasdc1iget38p",                "processorName": "STRIPE",                "processorMerchantId": "acct_1GORasvasdNWFwi8c",                "processorStatus": "SETTLED"            }        ]    }}

See the migration guide for how to update to the latest versions of the webhook event.

The order of the webhooks is not guaranteed. However, you can determine the latest status change of a payment by checking if payment.dateUpdated is newer compared to the last webhook received.


Refund notifications are triggered when a refund reaches a final state.

Check the most recent REFUND transaction in the transactions.status object to view the outcome:

  • if SETTLED, the refund was successful and the funds have been returned to the customer.
  • if FAILED, the refund was unsuccessful.

Example refund notification:

{    "eventType": "PAYMENT.REFUND",    "date": "2023-02-21T15:37:16.367687",    "notificationConfig": {        "id": "cc51f9f0-7e1c-492b-9d37-f83a818f6070",        "description": "Refund webhook"    },    "version": "2.1",    "payment": {        "id": "DdRZ6YY0",        "date": "2023-02-21T15:36:16.167687",        "dateUpdated": "2023-02-21T15:37:16.267687",        "amount": 3000,        "currencyCode": "GBP",        "customerId": "cust-123",        "orderId": "order-123",        "status": "SETTLED",        "paymentMethod": {            "paymentMethodToken": "-lcWjvBASh2EpYaHgVwxNjUzNTYzNjIy",            "analyticsId": "LUi5pETUV2EpYaHgV77SEamK25L",            "paymentMethodType": "PAYMENT_CARD",            "paymentMethodData": {                "last4Digits": "1111",                "expirationMonth": "03",                "expirationYear": "2030",                "cardholderName": "ADYEN",                "network": "Visa",                "isNetworkTokenized": false,                "binData": {                    "network": "VISA",                    "issuerCountryCode": "US",                    "issuerName": "JPMORGAN CHASE BANK, N.A.",                    "regionalRestriction": "UNKNOWN",                    "accountNumberType": "UNKNOWN",                    "accountFundingType": "UNKNOWN",                    "prepaidReloadableIndicator": "NOT_APPLICABLE",                    "productUsageType": "UNKNOWN",                    "productCode": "UNKNOWN",                    "productName": "UNKNOWN"                },                "cvvAvailable": true            },            "threeDSecureAuthentication": {                "responseCode": "NOT_PERFORMED"            }        },        "processor": {            "name": "STRIPE",            "processorMerchantId": "acct_1G2EpYaHgVZqNWFwi8c",            "amountCaptured": 3000,            "amountRefunded": 3000        },        "transactions": [            {                "date": "2023-02-21T15:36:16.167687",                "amount": 3000,                "currencyCode": "GBP",                "transactionType": "SALE",                "processorTransactionId": "pi_3L3ed23NWFwiNWFwi8c1iget38p",                "processorName": "STRIPE",                "processorMerchantId": "acct_1GORcaGv23NWFwi8c",                "processorStatus": "SETTLED"            },            {                "date": "2023-02-21T15:37:16.267687",                "amount": 3000,                "currencyCode": "GBP",                "transactionType": "REFUND",                "processorTransactionId": "pi_3L3ed23NWFwiNWFwi8c1iget38p",                "processorName": "STRIPE",                "processorMerchantId": "acct_1GORcaGv23NWFwi8c",                "processorStatus": "SETTLED"            }        ]    }}

Disputes & chargebacks

Dispute notifications trigger on newly opened disputes or chargebacks.

  1. eventType
The type of event that triggered the webhook. This will have the value DISPUTE.OPENED. This indicates that a dispute notification or chargeback was issued through a configured connection.

The easiest actions you can take are to proactively communicate with your customer and issue refunds where appropriate, especially when requested.
  1. primerAccountId
A unique identifier for your Primer merchant account.
  1. transactionId
A unique identifier for the Primer transaction corresponding to this dispute.
  1. orderId
Your reference for the sale transaction that the dispute relates to.
  1. processorId
The name of the processor that generated the dispute.
  1. processorDisputeId
A unique identifier for the corresponding connection dispute.
  1. paymentId
A unique identifier for the Primer payment corresponding to this dispute.

Example dispute notification:

{    "eventType": "DISPUTE.OPENED",    "version": "2.1",    "primerAccountId": "7fcd50f1-99f2-416e-8013-6ecd1c1285c3",    "transactionId": "c3f662ad-d197-492e-b78b-63eefa64a31d",    "orderId": "order-123",    "processorId": "Adyen",    "processorDisputeId": "adyen_ref_123",    "paymentId": "ecb8d3bc-805d-4d97-826e-ef8d4cc3d2a2",    "raw_processor_callback": {        "example": {            "raw_callback": "request",            "from": "Adyen"        }    }}

Workflow Run failed

Even if you already listen to the Payment status updates webhooks, consuming this additional webhook is strongly encouraged if you use Capture or Cancel via Workflows, as a failure to capture or cancel will neither fail the payment itself nor lead to an update of the payment status. Not knowing that a capture or cancel failed could therefore result in unintended consequences.

Full details available within the Automation documentation: Workflow Run failed webhook.

{    "eventType": "WORKFLOW_RUN.FAILED",    "version": "1.0",    "date": "2024-02-21 15:36:16.167687",    "primerAccountId": "123abcde-99f2-416e-8013-6ecd1c1285c3",    "triggerEventId": "DdRZ6YY0",    "workflow": {        "id": "ecb8d3bc-123a-4d56-826e-ef8d4cc3d2a2",        "name": "MIT UK Card",        "version" : 8    },    "run": {        "timestamp": "2024-03-07T12:20:14.394429",        "id": "bbb1c3cc-805d-4d97-826e-ef8d4cc3d2a2",        "status": "FAILED",        "lastError": {            "applicationId": "PRIMER_PAYMENTS",            "actionId": "capture_payment",            "diagnosticsId": "1234567890",            "message": "Payment ID not found."        }    }}

Webhook signing

Primer can sign all webhook notification events before they are sent to your server, allowing you to verify that the events were sent by us and not anyone else. This is achieved by adding a X-Signature-Primary header to each event, after which you can verify the signature.

How does it work?

A X-Signature-Primary header is added to all webhook events and is a HMAC signature generated using the webhook payload and a shared signing secret. This is then converted to a base64 encoded string.

The shared signing secret is generated by Primer and is unique to your account (details below). You will use this secret to validate the signature sent by Primer.

When you have rotated your secret, a X-Signature-Secondary header will also be added. See more below.

Avoid replay attacks

Inside the webhook payload there is an additional field signedAt to indicate the Unix timestamp of when the webhook was signed.

Use this timestamp to drastically reduce the chance of a replay attack. Because the timestamp is included in the payload, the same payload cannot be sent with a different timestamp without causing an invalid signature.

When verifying the signature, you should also validate that the timestamp is within an acceptable threshold from your current system time (in unix epoch format). Primer recommends a threshold of up to 3 minutes.

If Primer retries sending the webhook notification event, the timestamp at the time of sending the retried event is used, so each attempt would have a new timestamp and therefore a new hash.

Verifying the signature

To ensure that a webhook notification did indeed come from Primer, you need to compute the hash of the payload and compare that with the value in the header.

Below is an example of doing exactly this.

import base64import hashlibimport hmac
SHARED_SECRET = 'OYCTN7OTUBE2CX3EBGB5QABJBFUXWD3A'  # Hardcoded for demonstration. Use an environment variable or secrets vault in production.WEBHOOK_PAYLOAD = '{"eventType": "PAYMENT.STATUS", "date": "2023-09-14 16:30:35.059215", "notificationConfig": {"id": "d9ad7ba8-97fa-4364-9aec-16ddb3c68b86", "description": "2.2"}, "payment": {"id": "ov1370cHi", "date": "2023-09-14T16:30:34.696933", "dateUpdated": "2023-09-14T16:30:34.696933", "amount": 100, "currencyCode": "EUR", "customerId": "Primer", "orderId": "1234", "status": "PENDING", "customer": {}, "paymentMethod": {"paymentType": "UNSCHEDULED", "authorizationType": "FINAL", "paymentMethodToken": "xGMIxQcIT32FwXsve_2UnnwxNjk0NzA5MDMz", "isVaulted": false, "analyticsId": "7qBrd-64V6ucGeNHm7KNjkFO", "paymentMethodType": "PAYMENT_CARD", "paymentMethodData": {"last4Digits": "1111", "first6Digits": "411111", "expirationMonth": "03", "expirationYear": "2030", "cardholderName": "MR FOO BAR", "network": "Visa", "binData": {"network": "VISA", "issuerCountryCode": "US", "issuerName": "JPMORGAN CHASE BANK, N.A.", "regionalRestriction": "NONE", "accountNumberType": "PRIMARY_ACCOUNT_NUMBER", "accountFundingType": "DEBIT", "prepaidReloadableIndicator": "NOT_APPLICABLE", "productUsageType": "CONSUMER", "productCode": "VISA", "productName": "VISA"}, "isNetworkTokenized": false}}, "processor": {"name": "STRIPE", "processorMerchantId": "acct_1GORcsGZqNWFwi8c", "amountCaptured": 0, "amountRefunded": 0}, "transactions": []}, "version": "2.2", "signedAt": "1694709036"}'WEBHOOK_SIGNATURE = 'aYgNWDnUmNZOA7EGWgU3cZk8YrDa4AIyuio85YhSswQ='

def validate_webhook_signature(webhook_payload: str, webhook_signature: str, shared_secret: str) -> bool:    mac =        key=shared_secret.encode("utf-8"),        msg=webhook_payload.encode("utf-8"),        digestmod=hashlib.sha256,    )    computed_signature = base64.b64encode(mac.digest()).decode("utf-8").strip()
    # Verify the hash matches the signature from the header    return computed_signature == webhook_signature

def __main__():    if validate_webhook_signature(WEBHOOK_PAYLOAD, WEBHOOK_SIGNATURE, SHARED_SECRET):        print("Webhooks match!")    else:        print("Webhooks do NOT match!")


Set up your signing secret

Primer allows you to generate a unique signing secret key for all your webhook notification events for a specific environment (i.e. one for Sandbox and one for Production). After you have completed the setup (see below), Primer starts to sign each webhook it sends to your endpoints.

First, navigate to the Webhooks section on the Developers page of the Primer Dashboard. Then, select the Webhook signing button.

create webhook secret 2

Now you need to select the create signing secret button which will then generate your new signing secret.

create webhook secret 1

Once generated, your new signing secret will be shown.

This is your only chance to copy it, so make sure you store it somewhere safe!

create webhook secret 3

From this point, we will add the signature to all your webhook events and you can verify the signature to confirm the events are coming from Primer and not anyone else.

Rotate your signing secret

You can rotate the active signing secret from the Primer Dashboard at your discretion.

When a secret is rotated, the previous secret will remain active for 24 hours. During this period you will receive two signature headers and can verify against either.

  • X-Signature-Primary → The payload hashed with the new signing secret
  • X-Signature-Secondary → The payload hashed with the previous signing secret

This will give you time to update the stored signing secret within your application before the previous signing secret expires.

First, select the webhook signing button on the Webhooks section of the Developer page in the Primer Dashboard.

create webhook secret 4

Then, select the Regenerate secret button.

create webhook secret 5

A popup will then appear asking you to confirm you want to regenerate your signing secret. If you’re sure you want to proceed, select the Regenerate secret button.

This is an irreversible action so make sure you want to confirm the rotation of your signing secret.

create webhook secret 6

You will then be shown your new signing secret.

This is your only chance to copy it, so make sure you copy and paste it somewhere safe!

create webhook secret 7

Both signatures will be included in the webhook header for 24 hours to give you time to update the stored signing secret in your application.

When setting up handling the webhook events from Primer, it’s recommended to check for both headers so that you don’t experience any disruption when you rotate your signing secret.

1234567891011121314    "/my-webhook",)async def my_webhook(    request: Request,    primer_payload: dict):    if "X-Signature-Primary" in request.headers:        _validate_webhook_signature(request.headers["X-Signature-Primary"], primer_payload)
    if "X-Signature-Secondary" in request.headers:        _validate_webhook_signature(request.headers["X-Signature-Secondary"], primer_payload)
    # Process webhook...

This way, you will only need to update the stored secret within 24 hours and no further action will be needed.