Skip to main content
Migo Docs

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.

A flow of its own for Payment Link

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.callback through the Update Client Config flow.

For Migo-assisted configuration, send:

  1. Endpoint URL. The URL on your side that will receive the POST notifications (for example https://api.yourdomain.com/webhooks/transactions).
  2. Security headers (optional). Any custom HTTP headers Migo should include so you can authenticate the request β€” typically a static x-api-key or Authorization: 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"
}
}
}
}
}
FieldTypeRequiredDescription
config.callback.enabledbooleanYesEnables the callback configuration for the client.
config.callback.typestringYesConfigured callback type. For this contract, use generic.
config.callback.functionNamestringConditionalFunction 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.statusarray of stringsYesTransaction 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.urlstringYesAbsolute URL for your receiver. It must be valid and non-empty.
config.callback.data.methodstringYesHTTP method Migo will use to call your endpoint. It must be a method supported by the callback configuration.
config.callback.data.headersobjectYesHeaders 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.dataobjectYesBody template that Migo resolves against the transaction before sending the callback. It must be an object.
config.callback.data.extraDataobjectNoAdditional configuration data for the callback mechanism.

Configuration validations:

  • url must be a valid, non-empty URL.
  • method must be a supported HTTP method.
  • headers must be an object.
  • data must be an object.
  • extraData is optional.
  • status must be a non-empty array with supported statuses.
  • If callback is invalid, the update is not persisted.
Sensitive headers

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 sourceStatuses 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"
}
FieldTypeMax lengthDescriptionExample
referencestring100Migo-issued reference for the transaction. Returned in the response of POST /transactions; for POST /transactions-hook it is delivered here.ORDER-98765
uidstring100Unique transaction identifier issued by Migo. Same value returned by the create-link call.ak_D3b0ETlw3Hw...
countrystring2ISO 3166-1 alpha-2 country code where the operation took place.GT
currencystring3ISO 4217 currency code.GTQ
channelstring100Channel or platform where the transaction originated.WhatsApp
statusstring20Terminal status of the transaction. One of approved, denied, reversed, refunded.approved
amountinteger12Total transaction amount in the smallest currency unit.150
externalIdstring100Authorization code or external identifier returned by the payment processor.ext_auth_123
createdAtstring19Timestamp when the transaction was originally created, formatted as DD/MM/YYYY HH:MM:SS.05/03/2026 07:22:32
transactionIdstring100Internal transaction id used by the processor.txn_1029384756
paymentMethodTypestring100Type 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 2xx once 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:

FieldTypeDescription
enabledbooleanMust be true for the callback to fire.
typestringOne of "generic", "queue", "lambda". Selects the dispatch mechanism β€” see Variants of type.
functionNamestringRequired when type is "lambda". Name of the function Migo invokes to deliver the callback. The literal %env% is replaced by the deployment environment.
statusstring[]Transaction states configured for the callback. Runtime enforcement depends on the callback mechanism enabled for the client.
notifyExpirationbooleanOptional. Controls whether expired transactions also notify; behavior depends on the originating service.
dataobjectThe GenericCallback payload above (method, url, headers, body, timeout).

Variants of type​

typeImplementationMechanism
"generic"V1Migo hands the callback request to an internal dispatch mechanism that performs the HTTP call to your endpoint.
"queue"V1Same dispatch path as "generic" β€” uses the internal dispatch mechanism.
"lambda"V1Migo 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)V2Migo 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 the url field. It substitutes placeholders such as {{uid}}, {{reference}}, {{status}} with their values from the transaction.
  • getDataCallback(template, transaction) is applied to headers and data (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 sourceMechanism
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.