openapi: 3.1.0 info: title: DzWallet Payment API version: 1.1.0 description: | Accept CIB and Edahabia card payments in DZD through SATIM, create payment links and QR codes, check payment status and receive signed notifications (IPN). **Test mode** never contacts a real gateway: use `/test/payment/initiate` with your TEST public key. **Live mode** uses `/payment/initiate` with your LIVE public key (live keys exist only after KYC). Never put the secret key in a browser or mobile app. Registered domains: `ipn_url`, `success_url` and `cancel_url` must belong to a domain registered in your merchant profile. Rate limit: 60 requests per minute per IP (HTTP 429). contact: name: DzWallet url: https://dzwallet.app servers: - url: https://dzwallet.app tags: - name: Payments - name: Payment links paths: /payment/initiate: post: tags: [Payments] operationId: initiatePayment summary: Start a live payment description: Returns the checkout address (`redirect_url`). Send the customer there. Never retry automatically after a timeout; call the status endpoint with the same identifier first. requestBody: required: true content: application/x-www-form-urlencoded: schema: { $ref: '#/components/schemas/InitiateRequest' } responses: '200': description: Payment initiated content: application/json: schema: { $ref: '#/components/schemas/InitiateResponse' } '4XX': { $ref: '#/components/responses/Error' } /test/payment/initiate: post: tags: [Payments] operationId: initiateTestPayment summary: Start a test payment (simulated, no real gateway) description: Same request as the live endpoint. Use your TEST public key. The checkout page is simulated and a real signed IPN is sent, signed with the TEST secret key. requestBody: required: true content: application/x-www-form-urlencoded: schema: { $ref: '#/components/schemas/InitiateRequest' } responses: '200': description: Test payment initiated content: application/json: schema: { $ref: '#/components/schemas/InitiateResponse' } '4XX': { $ref: '#/components/responses/Error' } /api/payment/status: post: tags: [Payments] operationId: paymentStatus summary: Status of a payment description: Only `payment_status = success` proves the money was received. Safe to retry. requestBody: required: true content: application/x-www-form-urlencoded: schema: type: object required: [public_key, identifier] properties: public_key: { type: string, example: live_xxxxxxxx } identifier: { type: string, maxLength: 20, example: ORD8E532449302689 } responses: '200': description: Payment status content: application/json: schema: { $ref: '#/components/schemas/StatusResponse' } '4XX': { $ref: '#/components/responses/Error' } /api/payment-links: post: tags: [Payment links] operationId: createPaymentLink summary: Create a payment link (and QR code) description: The same `reference` with the same amount returns the same link. Requires LIVE keys. security: - publicKey: [] secretKey: [] requestBody: required: true content: application/x-www-form-urlencoded: schema: type: object required: [amount] properties: amount: { type: number, example: 2500, description: Amount in DZD } note: { type: string, maxLength: 100 } days: { type: integer, minimum: 1, maximum: 30, default: 7 } reference: { type: string, maxLength: 15, pattern: '^[A-Za-z0-9_-]{1,15}$', example: ORD1042 } ipn_url: { type: string, format: uri, description: Must belong to a registered domain } email: { type: string, format: email, description: The link is sent to this email } responses: '200': description: Link created content: application/json: schema: { $ref: '#/components/schemas/LinkResponse' } '4XX': { $ref: '#/components/responses/Error' } /api/payment-links/{reference}: get: tags: [Payment links] operationId: getPaymentLink summary: Status of a payment link security: - publicKey: [] secretKey: [] parameters: - { name: reference, in: path, required: true, schema: { type: string }, description: Your reference or the transaction number } responses: '200': description: Link content: application/json: schema: { $ref: '#/components/schemas/LinkResponse' } '4XX': { $ref: '#/components/responses/Error' } /api/payment-links/{reference}/reactivate: post: tags: [Payment links] operationId: reactivatePaymentLink summary: Reactivate an expired or cancelled link description: The same link and QR code work again and the validity restarts. security: - publicKey: [] secretKey: [] parameters: - { name: reference, in: path, required: true, schema: { type: string } } responses: '200': description: Link reactivated content: application/json: schema: { $ref: '#/components/schemas/LinkResponse' } '4XX': { $ref: '#/components/responses/Error' } webhooks: paymentNotification: post: summary: Instant payment notification (IPN) description: | Sent to your `ipn_url` (form-encoded, 15 second timeout, no redirects) after the gateway confirmed the payment. **Signature v2 (recommended).** Header `X-DzWallet-Signature: t=,v1=` where `v1 = HMAC_SHA256(SECRET_KEY, t + "." + raw request body)`. It covers the whole body (status, amount, transaction, event id, timestamp). Use the test secret for test payments and the live secret for live payments. Compare in constant time and reject the notification if `t` is more than 5 minutes from your clock. **Event id.** `event_id` (also sent as header `X-DzWallet-Event-Id`) is unique per payment and identical on every retry and manual redelivery. Read it from the signed body, store it with a unique constraint and ignore an event you already processed. Never fulfil an order twice. **Also check** that `identifier` is the one you created and that `data.amount` equals your order amount. **Retries.** If you do not answer with a 2xx status within 15 seconds, DzWallet retries after 1, 2, 4, 8, 16 and 32 minutes, then every hour, up to 15 attempts (about 12 hours). Every retry has a new timestamp and signature and the same event id. If all attempts fail, confirm the payment with `POST /api/payment/status`. Every attempt is listed in the merchant dashboard (Webhook Log), where a notification can be sent again. **Older signature.** The body field `signature == UPPERCASE(HMAC_SHA256(data.amount + identifier, SECRET_KEY))` is still sent for existing integrations. It does not cover the timestamp and cannot stop replays. parameters: - { name: X-DzWallet-Signature, in: header, required: true, schema: { type: string, example: 't=1700000000,v1=ab58...' }, description: 'Signature v2' } - { name: X-DzWallet-Event-Id, in: header, schema: { type: string, example: evt_f480cfa256b9114a0817eb63c8bcbc84 } } - { name: X-DzWallet-Timestamp, in: header, schema: { type: integer }, description: 'Unix time of this attempt' } - { name: X-DzWallet-Delivery-Attempt, in: header, schema: { type: integer }, description: '1 for the first delivery, then 2, 3 and so on' } requestBody: content: application/x-www-form-urlencoded: schema: { $ref: '#/components/schemas/IpnPayload' } responses: '200': { description: Acknowledged } components: securitySchemes: publicKey: { type: apiKey, in: header, name: X-Public-Key } secretKey: { type: apiKey, in: header, name: X-Secret-Key } responses: Error: description: Error (invalid keys, validation, domain rules, rate limit 429) content: application/json: schema: { $ref: '#/components/schemas/ErrorResponse' } schemas: InitiateRequest: type: object required: [public_key, identifier, amount, currency, details, ipn_url, success_url, cancel_url, site_name, customer] properties: public_key: { type: string, description: "TEST key on the test endpoint, LIVE key on the live endpoint" } identifier: { type: string, maxLength: 20, description: Your unique order identifier } currency: { type: string, enum: [DZD], default: DZD } amount: { type: number, example: 1000 } details: { type: string, maxLength: 100 } ipn_url: { type: string, format: uri } success_url: { type: string, format: uri } cancel_url: { type: string, format: uri } site_name: { type: string } site_logo: { type: string, format: uri } checkout_theme: { type: string, enum: [light, dark] } gateway_methods: type: array items: { type: string, enum: [Satim] } description: Sent as gateway_methods[] customer: type: object required: [first_name, last_name, email, mobile] properties: first_name: { type: string } last_name: { type: string } email: { type: string, format: email } mobile: { type: string } description: Sent as customer[first_name], customer[last_name], customer[email], customer[mobile] shipping_info: { type: object, additionalProperties: { type: string } } billing_info: { type: object, additionalProperties: { type: string } } InitiateResponse: type: object properties: status: { type: string, example: success } message: { type: array, items: { type: string }, example: [Payment initiated] } redirect_url: { type: string, format: uri } StatusResponse: type: object properties: status: { type: string, example: success } payment_status: { type: string, enum: [initiated, pending, success, rejected, cancelled] } identifier: { type: string } amount: { type: string } currency: { type: string } trx: { type: string } created_at: { type: string } LinkResponse: type: object properties: status: { type: string, example: success } message: { type: string, example: Payment link created } email_sent: { type: boolean } data: type: object properties: trx: { type: string, example: H3K9ABCD1234 } amount: { type: number, example: 2500 } currency: { type: string, example: DZD } status: { type: string, enum: [pending, paid, expired, cancelled] } url: { type: string, format: uri } print_url: { type: string, format: uri } expires_at: { type: string, example: '2026-11-04 10:00:00' } reference: { type: string, example: ORD1042 } identifier: { type: string, example: QRA03ORD1042, description: Value received as identifier in the IPN } ErrorResponse: type: object properties: status: { type: string, example: error } message: oneOf: - { type: string } - { type: array, items: { type: string } } IpnPayload: type: object properties: event: { type: string, example: payment.succeeded } event_id: { type: string, example: evt_f480cfa256b9114a0817eb63c8bcbc84 } timestamp: { type: integer, description: Unix time of this delivery attempt (signed) } status: { type: string, example: success } signature: { type: string, description: Older signature, kept for existing integrations } identifier: { type: string } data: type: object properties: payment_trx: { type: string } amount: { type: string, example: '1000.00000000' } payment_type: { type: string } payment_timestamp: { type: string } currency: { type: string } description: Sent as data[payment_trx], data[amount], ...