Merchant Generic Callback
The Merchant Generic Callback is the outbound HTTP request that Migo sends to a merchant-owned endpoint when a transaction reaches a terminal status. It is the second contract that your backend implements to integrate Payment Link: your backend creates the link (via POST /transactions or POST /transactions-hook) and later receives the result of the transaction on the URL you provide to Migo at integration time.
For most integrations Migo sends a standard JSON payload documented below. The dispatch is driven by per-client configuration so the request method, URL, headers, body, and timeout can be customized when needed; the standard payload covers the common case and is what you should design your handler around unless you have agreed on a custom shape with Migo.
The Merchant Generic Callback is a separate flow: it is the outbound notification of the Payment Link product. It does not sign the body with HMAC and it does not follow standardized retry semantics. Read it on its own terms.
How to configure your callbackβ
There are two ways to configure the callback:
- Migo-assisted configuration. Send the information to Migo via support ticket or your integration contact so Migo can register your endpoint.
- Self-service configuration. If your merchant has access enabled to Client Config Management, you can update
config.callbackthrough the Update Client Config flow.
For Migo-assisted configuration, send:
- Endpoint URL. The URL on your side that will receive the
POSTnotifications (for examplehttps://api.yourdomain.com/webhooks/transactions). - Security headers (optional). Any custom HTTP headers Migo should include so you can authenticate the request β typically a static
x-api-keyorAuthorization: Bearer <token>. By default Migo sends no extra headers; you must provide them explicitly if you want them.
Self-service configuration through Client Config Managementβ
When access is enabled for your merchant, the Update Client Config flow in Client Config Management (PUT /properties on that surface) can persist the callback configuration in the existing Client Config structure. Send callback inside config; if you do not send callback, Update Client Config keeps the previous behavior and only updates the properties you included.
status defines the transaction states configured for the callback. Runtime enforcement depends on the callback mechanism enabled for the client. Do not place it inside callback.data.status: it is persisted as callback.status.
Request example:
{
"client": "merchant-slug",
"source": "merchant-portal",
"config": {
"callback": {
"enabled": true,
"type": "generic",
"functionName": "generic",
"status": ["approved", "denied"],
"data": {
"url": "https://merchant.example.com/callback",
"method": "POST",
"headers": {
"x-api-key": "<merchant-secret>"
},
"data": {
"reference": "{{reference}}",
"uid": "{{uid}}",
"country": "{{country}}",
"currency": "{{currency}}",
"channel": "{{channel}}",
"status": "{{status}}",
"createdAt": "{{createdAt}}",
"amount": "{{total}}",
"externalId": "{{externalId}}"
},
"extraData": {
"source": "client-config"
}
}
}
}
}
| Field | Type | Required | Description |
|---|---|---|---|
config.callback.enabled | boolean | Yes | Enables the callback configuration for the client. |
config.callback.type | string | Yes | Configured callback type. For this contract, use generic. |
config.callback.functionName | string | Conditional | Function name or mechanism configured for the callback. For generic callback, generic can be used; other mechanisms may require a value agreed with Migo. |
config.callback.status | array of strings | Yes | Transaction states configured for the callback. It must be a non-empty array with supported statuses, for example approved and denied. It is persisted as callback.status. |
config.callback.data.url | string | Yes | Absolute URL for your receiver. It must be valid and non-empty. |
config.callback.data.method | string | Yes | HTTP method Migo will use to call your endpoint. It must be a method supported by the callback configuration. |
config.callback.data.headers | object | Yes | Headers Migo will send to the receiver. It must be an object; use placeholders or static values only when they are agreed for your integration. |
config.callback.data.data | object | Yes | Body template that Migo resolves against the transaction before sending the callback. It must be an object. |
config.callback.data.extraData | object | No | Additional configuration data for the callback mechanism. |
Configuration validations:
urlmust be a valid, non-empty URL.methodmust be a supported HTTP method.headersmust be an object.datamust be an object.extraDatais optional.statusmust be a non-empty array with supported statuses.- If
callbackis invalid, the update is not persisted.
Headers such as Authorization, x-api-key, token, secret, password, or apiKey may contain credentials. They must not be exposed in full in logs, tickets, screenshots, or shared examples; use masked values or placeholders when documenting them.
When the callback firesβ
Migo emits the callback at the end of the transaction, only for terminal statuses:
| Trigger source | Statuses that fire the callback |
|---|---|
Payment flows (finalUpdate) | approved, denied, refunded, reversed |
| Services / credits flows (direct dispatch) | approved, denied |
You may receive the callback through either dispatch path depending on which Migo flow handled the transaction. Your handler should not assume a single source; design it to be idempotent on (uid, status).
Standard payloadβ
Migo sends a POST with Content-Type: application/json to the URL you registered. The body is:
{
"reference": "ORDER-98765",
"uid": "ak_D3b0ETlw3HwPmQ3MNK",
"country": "GT",
"currency": "GTQ",
"channel": "WhatsApp",
"status": "approved",
"amount": 150,
"externalId": "ext_auth_123",
"createdAt": "05/03/2026 07:22:32",
"transactionId": "txn_1029384756",
"paymentMethodType": "credit_card"
}
| Field | Type | Max length | Description | Example |
|---|---|---|---|---|
reference | string | 100 | Migo-issued reference for the transaction. Returned in the response of POST /transactions; for POST /transactions-hook it is delivered here. | ORDER-98765 |
uid | string | 100 | Unique transaction identifier issued by Migo. Same value returned by the create-link call. | ak_D3b0ETlw3Hw... |
country | string | 2 | ISO 3166-1 alpha-2 country code where the operation took place. | GT |
currency | string | 3 | ISO 4217 currency code. | GTQ |
channel | string | 100 | Channel or platform where the transaction originated. | WhatsApp |
status | string | 20 | Terminal status of the transaction. One of approved, denied, reversed, refunded. | approved |
amount | integer | 12 | Total transaction amount in the smallest currency unit. | 150 |
externalId | string | 100 | Authorization code or external identifier returned by the payment processor. | ext_auth_123 |
createdAt | string | 19 | Timestamp when the transaction was originally created, formatted as DD/MM/YYYY HH:MM:SS. | 05/03/2026 07:22:32 |
transactionId | string | 100 | Internal transaction id used by the processor. | txn_1029384756 |
paymentMethodType | string | 100 | Type of payment method used for the transaction (for example credit_card, bank_transfer). | credit_card |
Authenticating the request on your sideβ
The Merchant Generic Callback does not sign the body with HMAC. Authenticity is delegated to the merchant via the headers you registered with Migo β typically a static API key sent on Authorization or a custom header (for example x-api-key).
Verify the header in your handler before processing the body. If you need stronger guarantees (HMAC, signed JWT, mTLS), coordinate with Migo so the configuration can be extended for your client; it is not part of the standard contract.
Receiver skeleton (Node.js / Express)β
import express from 'express';
const app = express();
const MIGO_API_KEY = process.env.MIGO_WEBHOOK_API_KEY;
app.post(
'/webhooks/transactions',
express.json({ limit: '1mb' }),
async (req, res) => {
// 1. Authenticate the request via the header you registered with Migo.
if (req.header('x-api-key') !== MIGO_API_KEY) {
return res.status(401).send('invalid credentials');
}
const { uid, status, reference, amount, currency } = req.body;
// 2. Idempotency: dedupe on (uid, status). Migo may deliver the same
// notification more than once if a queued message is replayed.
if (await alreadyProcessed(uid, status)) {
return res.status(200).json({ ack: 'duplicate' });
}
// 3. Process based on req.body.status:
// - "approved" -> mark order as paid, fulfill, etc.
// - "denied" -> mark order as failed, notify the customer.
// - "refunded" -> reverse fulfillment, update accounting.
// - "reversed" -> idem.
await processCallback(req.body);
// 4. Respond 2xx so Migo records a successful delivery.
res.status(200).json({ ack: 'received' });
},
);
Recommendations:
- Always respond
2xxonce your processing is durable; failing the response makes Migo log a delivery error. - Make the handler idempotent on
(uid, status). Treat repeat deliveries as a normal case. - Validate the schema against the standard payload above; only extend it if you have agreed on a custom payload with Migo.
- Keep your processing fast or move it to a queue inside your system. Migo applies a default timeout of 25 seconds.
Behavior on errorβ
Migo logs delivery failures and persists request / response steps for the operator to reprocess manually if needed. Retry semantics depend on the dispatch path Migo selected for your client (some flows hand the request to an internal dispatch mechanism with its own retry policy; others perform a single direct call). Treat each delivery as best-effort and rely on idempotency on your side.
Advanced: custom payload templatesβ
Most merchants do not need this section. It documents the per-client configuration that drives the dispatch and lets you customize the request when the standard payload is not enough.
The callback is configured per client in the ClientConfig.callback document. The shape is:
interface GenericCallback {
// HTTP method Migo uses to call your endpoint. Typically "POST".
method: string;
// Merchant endpoint URL. Supports template placeholders resolved with
// transaction values via getValues().
url: string;
// Optional template headers. Each value can include placeholders resolved
// with transaction values via getDataCallback().
headers?: { [key: string]: string };
// Optional template body. Recursively resolved with transaction values
// via getDataCallback().
data?: { [key: string]: any };
// Optional extra configuration for the callback mechanism.
extraData?: { [key: string]: any };
// Optional timeout in milliseconds. Defaults to 25000.
timeout?: number;
}
The wrapper that lives on the client config adds these fields:
| Field | Type | Description |
|---|---|---|
enabled | boolean | Must be true for the callback to fire. |
type | string | One of "generic", "queue", "lambda". Selects the dispatch mechanism β see Variants of type. |
functionName | string | Required when type is "lambda". Name of the function Migo invokes to deliver the callback. The literal %env% is replaced by the deployment environment. |
status | string[] | Transaction states configured for the callback. Runtime enforcement depends on the callback mechanism enabled for the client. |
notifyExpiration | boolean | Optional. Controls whether expired transactions also notify; behavior depends on the originating service. |
data | object | The GenericCallback payload above (method, url, headers, body, timeout). |
Variants of typeβ
type | Implementation | Mechanism |
|---|---|---|
"generic" | V1 | Migo hands the callback request to an internal dispatch mechanism that performs the HTTP call to your endpoint. |
"queue" | V1 | Same dispatch path as "generic" β uses the internal dispatch mechanism. |
"lambda" | V1 | Migo invokes a specific named function (functionName) that is responsible for delivering the callback to your endpoint. Use this only when you have agreed with Migo on a custom dispatch function. |
(no type field) | V2 | Migo calls your endpoint directly with a single axios request from the originating service. |
For the merchant, the request that lands on your endpoint looks the same in V1 (generic / queue) and V2: the URL, method, headers, and body resolved from your configuration. The differences are operational (queueing, retries, error handling β see Behavior on error).
Templating rulesβ
The configuration values are templates. Migo substitutes placeholders against the transaction object before sending the request.
getValues(template, transaction)is applied to theurlfield. It substitutes placeholders such as{{uid}},{{reference}},{{status}}with their values from the transaction.getDataCallback(template, transaction)is applied toheadersanddata(the body). It walks the object recursively and substitutes placeholders inside every string value.
Placeholders available at runtime correspond to fields on the transaction object that Migo persists for the payment, including (non-exhaustive): uid, reference, status, total, currency, customKeys, userId, clientCode, and the timestamps emitted by Migo. Only fields that exist on the transaction are substituted; missing fields are left as the literal placeholder, so design your templates around the fields you have agreed with Migo.
Configuration exampleβ
{
"enabled": true,
"type": "generic",
"functionName": "generic",
"status": ["approved", "denied"],
"data": {
"method": "POST",
"url": "https://api.merchant.com/migo/callback?trx={{uid}}",
"headers": {
"Authorization": "Bearer <merchant-static-api-key>",
"Content-Type": "application/json"
},
"data": {
"transactionUid": "{{uid}}",
"reference": "{{reference}}",
"status": "{{status}}",
"amount": "{{total}}",
"currency": "{{currency}}",
"metadata": "{{customKeys}}"
},
"extraData": {
"source": "client-config"
},
"timeout": 30000
}
}
When a transaction with uid = "abcd1234efgh" reaches approved, Migo sends a POST to https://api.merchant.com/migo/callback?trx=abcd1234efgh with the Authorization header above and a JSON body where every placeholder is resolved against the transaction.
Dispatch internalsβ
| Trigger source | Mechanism |
|---|---|
| Payment flows (V1) | Migo hands the request to an internal dispatch mechanism with its own retry policy at the infrastructure level. |
| Services / credits flows (V2) | Migo performs a single direct call from the originating service. On HTTP error or timeout the failure is logged and the request / response are persisted as transaction steps; there is no in-process retry. |
The exact retry semantics are operational and may evolve. Coordinate with Migo if your business requires specific delivery guarantees.
Relatedβ
- Create a Payment Link β the inbound contract you call to obtain the webview URL.
- Hosted checkout (webview) β the customer payment experience for Payment Link.