Paiement direct — sans redirection
Par défaut, l'encaissement Tchin fonctionne par redirection : vous créez un paiement, puis vous envoyez le client sur notre page de paiement hébergée. Simple, rien à gérer côté design ni sécurité.
Le paiement direct vous donne une seconde option : vous construisez votre propre page de paiement (votre design, votre tunnel) et vous déclenchez la charge depuis votre serveur, sans jamais rediriger le client vers Tchin. En contrepartie, c'est vous qui gérez le choix de l'opérateur et le déroulé propre à chaque moyen (certains valident par notification sur le téléphone, d'autres par code OTP, d'autres par redirection vers l'opérateur).
Quel mode choisir ?
| Critère | Redirection (hébergé) | Direct (cette page) |
|---|---|---|
| Effort d'intégration | Minimal | Plus élevé (vous gérez l'UI et les flux) |
| Design de la page de paiement | Fourni par Tchin | 100 % le vôtre |
| Choix de l'opérateur | Fait par le client sur notre page | Fait chez vous (via /methods) |
| Gestion OTP / redirection opérateur | Gérée par Tchin | À votre charge |
| Sécurité (PCI, secrets) | Aucune exposition | Appels serveur uniquement |
Bonne nouvelle : c'est le même paiement. Vous créez toujours le paiement avec POST /payments. Ensuite, soit vous redirigez vers payment_url (mode hébergé), soit vous appelez /charge (mode direct). Vous pouvez donc proposer les deux dans la même appli.
Le principe en 4 étapes
- Lister les opérateurs disponibles par pays —
GET /api/v1/methods. - Créer le paiement —
POST /api/v1/payments→ vous récupérez untoken. - Charger directement —
POST /api/v1/payments/{token}/chargeavec l'opérateur + le numéro. - Suivre le résultat —
GET /api/v1/payments/{token}/statuset/ou votre webhook.
Étape 1 — Lister les moyens de paiement
GET /api/v1/methods renvoie, pour chaque pays, les opérateurs et surtout leur type de flux (flow). C'est ce champ qui vous dit quoi faire sur votre page.
{
"success": true,
"countries": [
{
"country": "CI",
"country_name": "Côte d’Ivoire",
"dial": "+225",
"methods": [
{ "code": "orange-money-ci", "name": "Orange Money", "flow": "otp", "needs_otp": true, "available": true },
{ "code": "wave-ci", "name": "Wave", "flow": "redirect", "needs_otp": false, "available": true },
{ "code": "mtn-ci", "name": "MTN", "flow": "push", "needs_otp": false, "available": true }
]
}
]
}
| Champ | Description |
|---|---|
code | Identifiant de l'opérateur à renvoyer dans /charge (ex. orange-money-ci). |
flow | push, otp ou redirect — voir le tableau plus bas. |
needs_otp | true si le client doit fournir un code OTP (équivaut à flow = "otp"). |
available | false si l'opérateur est momentanément hors service — désactivez-le dans votre UI. |
Étape 2 — Créer le paiement
Identique au mode hébergé : POST /api/v1/payments avec amount (et éventuellement return_url, callback_url…). Voir la page Encaissement. Vous récupérez un token — gardez-le.
Astuce : pour les opérateurs à flow = "redirect" (Wave, Orange Money QR…), définissez un return_url à la création : le client y sera renvoyé sur votre site après avoir payé chez l'opérateur.
Étape 3 — Charger directement
POST /api/v1/payments/{token}/charge
| Paramètre | Requis | Description |
|---|---|---|
country | oui | Pays du client (code ISO 2 lettres, ex. CI). |
operator | oui | Le code d'un moyen renvoyé par /methods. |
phone | oui | Numéro mobile money du client. |
otp | si flow = otp | Code de paiement fourni par le client (voir le flux OTP). |
password | en test | En mode test uniquement : mot de passe de votre compte de test (voir plus bas). |
customer_name / customer_email | non | Optionnels ; générés automatiquement s'ils sont absents. |
# 1) Créez d'abord le paiement (voir « Encaissement ») → vous obtenez un "token".
# 2) Puis chargez-le DIRECTEMENT, sans rediriger le client :
curl -X POST https://tchin.tech/api/v1/payments/TOKEN/charge \
-H "TCHIN-PUBLIC-KEY: tchin_pk_xxxxxxxx" \
-H "TCHIN-PRIVATE-KEY: tchin_sk_xxxxxxxx" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"country": "CI",
"operator": "orange-money-ci",
"phone": "0700000000",
"otp": "123456"
}'
<?php
// Paiement DIRECT — CÔTÉ SERVEUR uniquement (ne jamais exposer la clé secrète).
// $token provient de POST /api/v1/payments.
$token = 'e56praamokkkk5';
$body = [
'country' => 'CI', // pays du client (code ISO 2 lettres)
'operator' => 'orange-money-ci', // code d'un moyen renvoyé par GET /methods
'phone' => '0700000000', // numéro mobile money du client
'otp' => '123456', // REQUIS seulement si le moyen a flow = "otp"
// 'customer_name' => 'Awa Diop', // optionnels
// 'customer_email' => 'awa@mail.com',
];
$ch = curl_init("https://tchin.tech/api/v1/payments/{$token}/charge");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'TCHIN-PUBLIC-KEY: ' . getenv('TCHIN_PUBLIC_KEY'),
'TCHIN-PRIVATE-KEY: ' . getenv('TCHIN_SECRET_KEY'),
'Content-Type: application/json',
'Accept: application/json',
],
CURLOPT_POSTFIELDS => json_encode($body),
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
// On agit selon "action" renvoyé :
switch ($data['action'] ?? null) {
case 'confirm_on_phone':
// Affichez « Validez le paiement sur votre téléphone », puis interrogez le statut.
break;
case 'redirect':
// Redirigez le client vers l'opérateur (Wave, Orange Money QR…).
header('Location: ' . $data['redirect_url']);
exit;
case 'otp_required':
// Faites composer le code USSD au client, récupérez l'OTP, puis renvoyez CETTE requête avec "otp".
break;
default:
if (! ($data['success'] ?? false)) {
echo $data['message'] ?? 'Paiement refusé.';
}
}
// Paiement DIRECT (Node 18+) — CÔTÉ SERVEUR. token vient de POST /api/v1/payments.
const token = 'e56praamokkkk5';
const r = await fetch(`https://tchin.tech/api/v1/payments/${token}/charge`, {
method: 'POST',
headers: {
'TCHIN-PUBLIC-KEY': process.env.TCHIN_PUBLIC_KEY,
'TCHIN-PRIVATE-KEY': process.env.TCHIN_SECRET_KEY,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
country: 'CI',
operator: 'orange-money-ci', // code renvoyé par GET /methods
phone: '0700000000',
otp: '123456', // seulement si flow === "otp"
}),
});
const data = await r.json();
switch (data.action) {
case 'redirect': return res.redirect(data.redirect_url); // Wave, OM…
case 'confirm_on_phone': /* afficher "validez sur le téléphone" + poller le statut */ break;
case 'otp_required': /* demander l'OTP au client, renvoyer avec otp */ break;
default: if (!data.success) console.error(data.message);
}
# Paiement DIRECT (requests) — CÔTÉ SERVEUR. token vient de POST /api/v1/payments.
import os, requests
token = 'e56praamokkkk5'
resp = requests.post(
f'https://tchin.tech/api/v1/payments/{token}/charge',
headers={
'TCHIN-PUBLIC-KEY': os.environ['TCHIN_PUBLIC_KEY'],
'TCHIN-PRIVATE-KEY': os.environ['TCHIN_SECRET_KEY'],
'Accept': 'application/json',
},
json={
'country': 'CI',
'operator': 'orange-money-ci', # code renvoyé par GET /methods
'phone': '0700000000',
'otp': '123456', # seulement si flow == "otp"
},
timeout=45,
)
data = resp.json()
action = data.get('action')
if action == 'redirect':
redirect(data['redirect_url']) # Wave, Orange Money…
elif action == 'confirm_on_phone':
pass # afficher "validez sur le téléphone" + poller le statut
elif action == 'otp_required':
pass # demander l'OTP, renvoyer avec "otp"
elif not data.get('success'):
print(data.get('message')) # échec
Gérer la réponse selon le flow
La réponse contient un champ action qui vous dit exactement quoi faire ensuite :
flow | action renvoyé | Ce que VOUS faites sur votre page |
|---|---|---|
push | confirm_on_phone | Affichez « Validez le paiement sur votre téléphone », puis interrogez le statut jusqu'à completed. |
otp | otp_required (si pas d'OTP) | Demandez au client de composer le code USSD de son opérateur, faites-lui saisir l'OTP reçu, puis rappelez /charge avec otp. |
redirect | redirect | Redirigez le client vers redirect_url (Wave, Orange Money…). Il revient ensuite sur votre return_url. |
Exemples de réponses
Validation sur téléphone (push) :
{
"success": true,
"token": "e56praamokkkk5",
"status": "pending",
"action": "confirm_on_phone",
"message": "Paiement en cours, le client doit valider sur son téléphone."
}
Redirection opérateur (Wave, OM QR…) :
{
"success": true,
"token": "e56praamokkkk5",
"status": "pending",
"action": "redirect",
"redirect_url": "https://pay.wave.com/c/cos-XXXXXXXX"
}
OTP requis — l'opérateur exige un code ; recommencez avec otp :
{
"success": false,
"token": "e56praamokkkk5",
"status": "failed",
"action": "otp_required",
"message": "Cet opérateur exige un code de paiement (OTP). Faites composer le code USSD au client, puis renvoyez la requête avec « otp »."
}
Échec :
{
"success": false,
"token": "e56praamokkkk5",
"status": "failed",
"message": "Solde insuffisant / opérateur momentanément indisponible."
}
Étape 4 — Confirmer le paiement
Un status: "pending" n'est pas un paiement confirmé. La confirmation finale arrive de deux façons (utilisez au moins l'une) :
- Webhook (recommandé) : Tchin appelle votre
callback_urldès que le paiement estcompletedoufailed. Voir Webhooks. - Polling : interrogez GET
/api/v1/payments/{token}/statustoutes les quelques secondes jusqu'à obtenircompletedoufailed.
Ne livrez jamais la commande sur un simple pending : attendez completed.
Mode test (sandbox)
En environnement de test (env: "test" à la création), l'appel /charge exige un champ password : le mot de passe de votre compte de test. Cela reproduit fidèlement un vrai paiement sans mouvement d'argent, et déclenche un webhook de test pour valider votre intégration de bout en bout.
Sécurité
- Tous ces appels contiennent votre clé secrète : effectuez-les uniquement côté serveur. Jamais dans une page web, une app mobile ou du JavaScript navigateur.
- Ne montrez jamais le numéro/OTP d'un client à un tiers ; ne journalisez pas les OTP.
- Vérifiez la signature de vos webhooks avant de créditer une commande.