API · Direct

Payment Intents

The core C2B charge object. One shape across Waafipay, CAC, SabaPay, D-Money, EAB, Cybersource card — and any future rail. State-machine driven, idempotent by contract.

5 endpointspi_

Endpoints

POST/v1/payment_intents

Create a new PaymentIntent.

GET/v1/payment_intents

List recent intents (limit ≤ 100, default 25).

GET/v1/payment_intents/{id}

Retrieve a specific intent.

POST/v1/payment_intents/{id}/confirm

Finalise a requires_action intent (OTP, redirect return).

POST/v1/payment_intents/{id}/cancel

Cancel an intent before settlement.

Create

NameTypeDescription
amount
required
integerMinor units. DJF is zero-decimal; pass integer DJF.
currency
required
ISO 4217"DJF", "USD", "ETB".
provider
required
enumwaafi · cac · sabapay · dmoney · eab · santimpay · mock
metadataobjectProvider flow selectors: waafi_mode (hpp wallet · card Visa/MC), santim_mode (direct) + santim_payment_method, auth_mode (SabaPay APP/OTP).
customer_msisdnE.164Required for mobile-money providers.
customer_accountstringRequired for bank providers (IBAN or other identifier).
customer_idcus_Optional — attach to a Customer for tokenised re-use.
descriptionstring≤140 chars, appears on receipts.
callback_urlurlWhere the provider sends async status updates (we proxy to your webhook).
return_urlurlWhere the customer is sent after a redirect-based confirm.
metadatamapFree-form string-string map; round-trips on every response.
Send Idempotency-Key: order_42 on every create. Same key in the next 24h returns the same intent — never a double-charge.

Confirm

For OTP-based rails, call confirm with the OTP after the customer enters it. For redirect-based rails the intent transitions on the provider callback; confirm is a no-op.

NameTypeDescription
otpstringFor OTP-based rails (Waafi, D-Money). Required when status was requires_action.
client_secretstringRequired when authed with pk_* (browser).
providerenumOptional override; defaults to the provider used on create.
metadatamapAdditional fields to merge into the intent.

Sample request + response

POST /v1/payment_intentsbash
curl -X POST https://api.merashub.com/v1/payment_intents \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: order_42" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 50000,
    "currency": "DJF",
    "provider": "waafi",
    "customer_msisdn": "+25377xxxxxxx",
    "description": "Order #42"
  }'
201 Createdjson
{
  "id": "pi_01HZF8AYJX3D8M7QK0E5V4WJ9K",
  "client_secret": "pi_01HZF8AYJX3D8M7QK0E5V4WJ9K_secret_xyz",
  "amount_minor": 50000,
  "currency": "DJF",
  "status": "requires_action",
  "next_action": {
    "type": "collect_otp",
    "otp_length": 6
  },
  "metadata": {},
  "created_at": "2026-05-25T10:15:30Z"
}

Status state machine

  • requires_payment_method

    New intent — caller has not picked a provider yet (rare; orchestrator usually moves past this immediately).

  • requires_action

    Awaiting OTP, redirect return, or external confirmation.

  • processing

    Provider has accepted; settlement in progress.

  • succeeded

    Terminal success. Ledger booked, webhook fired.

  • canceled

    Caller cancelled, OR provider declined.

Webhook is the source of truth

Never finalise an order on the confirm response alone — listen for payment_intent.succeeded on your webhook endpoint. The synchronous response may say processing while the rail is still settling asynchronously.