DealNear

DealNear Merchant API v1

La documentation de l’API est offerte en anglais.

Connect your point-of-sale system, booking software or website to DealNear. With the API you can publish and update deals, see which customers claimed coupons, and redeem coupons at checkout without opening the merchant center.

Base URL
https://www.dealnear.ca/api/v1
Format
JSON in and out, UTF-8. Times are ISO 8601; prices are CAD.
OpenAPI 3.1
openapi.json — import into Postman or Insomnia, or generate a client.

Authentication

Create a key in Merchant center → API keys. Each key belongs to your business and is shown only once, so store it like a password. Revoke it there at any time.

Send it as a bearer token with every request:

Authorization: Bearer dn_your_secret_key

Give each key only the permissions it needs. A key without the required permission gets 403 with "error": "missing_scope".

ScopeAllows
deals:readConsulter vos offres
deals:writeCréer, modifier, suspendre et remettre en ligne des offres
coupons:readConsulter les coupons obtenus par les clients
coupons:redeemValider les coupons à la caisse

Try your key:

curl https://www.dealnear.ca/api/v1/me \
  -H "Authorization: Bearer $DEALNEAR_API_KEY"

Redeeming at checkout

The most common integration: the customer shows their coupon code, your till looks it up and redeems it. This needs a key with coupons:read and coupons:redeem. Codes can be sent however the cashier typed them — dn7k4mq2xp works as well as DN-7K4M-Q2XP.

1. Look up the code (optional)

Shows the cashier which deal it is for, and whether it can be used (is_usable).

curl https://www.dealnear.ca/api/v1/coupons/DN-7K4M-Q2XP \
  -H "Authorization: Bearer $DEALNEAR_API_KEY"

2. Redeem it

curl -X POST https://www.dealnear.ca/api/v1/coupons/DN-7K4M-Q2XP/redeem \
  -H "Authorization: Bearer $DEALNEAR_API_KEY"

200 OK

{
  "data": {
    "code": "DN-7K4M-Q2XP",
    "status": "redeemed",
    "is_usable": false,
    "deal": { "id": 42, "title": "Hot pot for two, 20% off", "discount_label": "20% off" },
    "customer": { "name": "Jamie" },
    "claimed_at": "2026-10-12T18:04:11-04:00",
    "redeemed_at": "2026-10-15T12:31:09-04:00",
    "expires_at": "2026-11-15T23:59:59-05:00",
    "updated_at": "2026-10-15T12:31:09-04:00"
  }
}

A coupon can only be redeemed once, so it is safe to retry after a network error: a repeat returns 409 with the coupon and when it was redeemed. Tell the cashier why based on error:

StatuserrorMeaning
409already_redeemedUsed before — see data.redeemed_at.
409coupon_cancelledThe customer gave the coupon back.
409coupon_expiredThe deal has ended.
404not_foundNo such code at your business (mistyped, or a coupon for another business).

Creating deals

Write the deal in one language (locale) and optionally add translations; customers see their own language where you provided it. Get category_id and city_id from GET /categories and GET /cities.

New deals are pending until DealNear reviews them. Editing the text, prices or image of a live deal sends it back to review; dates and quantities can change freely.

curl -X POST https://www.dealnear.ca/api/v1/deals \
  -H "Authorization: Bearer $DEALNEAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "locale": "en",
    "content": {
      "en":    { "title": "Hot pot for two, 20% off", "discount_label": "20% off",
                 "description": "Any hot pot set for two people.", "fine_print": "Dine-in only." },
      "zh_CN": { "title": "双人火锅 8 折", "discount_label": "8折" }
    },
    "category_id": 1,
    "city_id": 4,
    "original_price": 69.99,
    "deal_price": 55.99,
    "ends_at": "2026-12-31T23:59:00-05:00",
    "total_quantity": 100,
    "per_user_limit": 1
  }'

To include a cover image, send the same fields as multipart/form-data (e.g. -F "content[en][title]=…" -F "[email protected]"). PNG, JPG or WebP, up to 4 MB.

Webhooks

Instead of polling, let DealNear tell your system when something changes: a coupon is claimed, redeemed or cancelled, or a deal is approved or rejected. Add up to 5 endpoints in Merchant center → API keys → Webhooks, choose the events each one receives, and use Send test event to check it.

Endpoints must be public https:// URLs; addresses on private networks are refused. For each event we send a POST with a JSON body. data has the same fields as the coupon or deal in the API, as it was when the event happened:

POST /dealnear/webhook HTTP/1.1
Content-Type: application/json
DealNear-Event: coupon.redeemed
DealNear-Event-Id: evt_01k7jz2q9w3v8x4c5n6m7p8r9t
DealNear-Delivery: 1842
DealNear-Signature: t=1760545869,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

{
  "id": "evt_01k7jz2q9w3v8x4c5n6m7p8r9t",
  "type": "coupon.redeemed",
  "created_at": "2026-10-15T12:31:09-04:00",
  "data": {
    "code": "DN-7K4M-Q2XP",
    "status": "redeemed",
    "is_usable": false,
    "deal": { "id": 42, "title": "Hot pot for two, 20% off", "discount_label": "20% off" },
    "customer": { "name": "Jamie" },
    "claimed_at": "2026-10-12T18:04:11-04:00",
    "redeemed_at": "2026-10-15T12:31:09-04:00",
    "expires_at": "2026-11-15T23:59:59-05:00",
    "updated_at": "2026-10-15T12:31:09-04:00"
  }
}

Responding and retries

  • Answer with any 2xx status within 10 seconds. Do slow work after you respond, e.g. in a queue.
  • Any other status, a timeout or a connection error is retried after 1 minute, 5 minutes, 30 minutes, then 1, 2, 4, 8 and 12 hours — 9 attempts over about 28 hours. Redirects are not followed.
  • An event can arrive more than once, and events can arrive out of order. Ignore an id you have already handled, and compare data.updated_at before overwriting newer data.
  • If an endpoint fails every attempt for a day, it is disabled and stops receiving events. Fix it, send a test event and enable it again in the merchant center, then catch up with GET /coupons?updated_since=….
  • The merchant center shows each endpoint’s recent deliveries and their response status. History is kept for 30 days.

Verifying signatures

Each endpoint has its own signing secret (whsec_…), shown once when you add the endpoint. Check every request before trusting it:

  1. Split the DealNear-Signature header on , into t (a Unix timestamp) and v1.
  2. Compute HMAC-SHA256 of {t}.{raw request body} with your secret, as lowercase hex. Use the body exactly as received, before any JSON parsing.
  3. Compare it with v1 in constant time, and reject timestamps more than 5 minutes from now to stop replays. Each retry is signed again with a fresh timestamp.

PHP

$payload = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_DEALNEAR_SIGNATURE'] ?? ''), $signature);

$expected = hash_hmac('sha256', ($signature['t'] ?? '').'.'.$payload, getenv('DEALNEAR_WEBHOOK_SECRET'));

if (! hash_equals($expected, $signature['v1'] ?? '') || abs(time() - (int) ($signature['t'] ?? 0)) > 300) {
    http_response_code(400);
    exit;
}

$event = json_decode($payload, true);

Node.js (Express)

import crypto from 'node:crypto';

app.post('/dealnear/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const { t, v1 = '' } = Object.fromEntries((req.get('DealNear-Signature') ?? '').split(',').map((part) => part.split('=')));
  const expected = crypto.createHmac('sha256', process.env.DEALNEAR_WEBHOOK_SECRET).update(`${t}.${req.body}`).digest('hex');

  if (v1.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
      || Math.abs(Date.now() / 1000 - Number(t)) > 300) {
    return res.sendStatus(400);
  }

  const event = JSON.parse(req.body);
  res.sendStatus(204);
});

Errors

Errors are JSON with a human-readable message and, where useful, a stable error code to branch on.

StatusWhen
401Missing, invalid or revoked API key.
403The key lacks the permission (missing_scope), or your business is suspended.
404Not found, or it belongs to another business (not_found).
409The action conflicts with the current state, e.g. redeeming a used coupon or pausing a pending deal (invalid_status).
422Validation failed. errors lists the messages per field, e.g. "errors": {"content.en.title": ["The Title (English) field is required."]}.
429Too many requests (rate_limited). Wait for the number of seconds in the Retry-After header.

Pagination & limits

  • Lists return data, links and meta. Use per_page (up to 100) and follow links.next until it is null.
  • To keep your own system in sync, use webhooks, or poll GET /coupons?updated_since=… with the time of your last sync.
  • Each key can make 120 requests per minute. Responses include X-RateLimit-Limit and X-RateLimit-Remaining.
  • Version 1 only gets backwards-compatible changes (new endpoints and new fields). Ignore fields you do not recognise.

Account

The business behind the API key.

GET /me any key

Check your API key

Returns the business the key belongs to and the key’s scopes. Use it to test a new key.

200 401

Coupons

Coupons customers have claimed from your deals.

GET /coupons coupons:read

List coupons

Coupons claimed from any of your deals, newest first. Poll with updated_since to sync redemptions and cancellations into your own system.

status query Only coupons in this status: claimed, redeemed or cancelled.
deal_id query Only coupons for this deal.
updated_since query Only coupons claimed, redeemed or cancelled at or after this time (ISO 8601).
per_page query Results per page (1–100, default 25).
page query Page number, starting at 1.
200 401 403 422
GET /coupons/{code} coupons:read

Look up a coupon

Check a code before redeeming it, e.g. to show the cashier which deal it is for. is_usable tells you whether it can be redeemed right now.

code path, required Coupon code as the customer shows it. Case, spaces and dashes are ignored: dn7k4mq2xp works as well as DN-7K4M-Q2XP.
200 401 403 404
POST /coupons/{code}/redeem coupons:redeem

Redeem a coupon

Marks the coupon as used. Each coupon can be redeemed once; it is safe to retry, because a repeat attempt returns 409 with error: already_redeemed and the coupon (including redeemed_at).

Possible error values on 409: already_redeemed, coupon_cancelled, coupon_expired.

code path, required Coupon code as the customer shows it. Case, spaces and dashes are ignored: dn7k4mq2xp works as well as DN-7K4M-Q2XP.
200 401 403 404 409

Deals

The offers you publish on DealNear.

GET /deals deals:read

List deals

All of your deals, newest first, in every status.

status query Only deals in this status: pending, active, rejected or paused.
per_page query Results per page (1–100, default 25).
page query Page number, starting at 1.
200 401 403 422
POST /deals deals:write

Create a deal

New deals start as pending and go live once DealNear has reviewed them. Send JSON, or multipart/form-data to include a cover image.

Body: deal fields as in Creating deals.

201 401 403 422
GET /deals/{deal} deals:read

Get a deal

deal path, required Deal ID.
200 401 403 404
PUT /deals/{deal} deals:write

Update a deal

Replaces the deal with the body you send (send every field, as when creating). Languages you leave out of content are removed.

Changing any text, a price or the image sends a live deal back to pending for review. Changing dates, quantities or the per-person limit does not.

deal path, required Deal ID.

Body: deal fields as in Creating deals.

200 401 403 404 422
POST /deals/{deal}/pause deals:write

Pause a deal

Hides a live deal from customers. Coupons already claimed stay valid. Pausing a paused deal does nothing.

deal path, required Deal ID.
200 401 403 404 409
POST /deals/{deal}/resume deals:write

Resume a deal

Puts a paused deal back online. Resuming a live deal does nothing.

deal path, required Deal ID.
200 401 403 404 409

Reference

IDs used when creating deals.

GET /categories any key

List categories

200 401
GET /cities any key

List cities

200 401

Webhook events

Requests DealNear sends to your endpoints when coupons and deals change. Every request has the headers and envelope shown in Webhooks.

EVENT coupon.claimed

Coupon claimed

A customer claimed a coupon from one of your deals. data is the coupon, as returned by GET /coupons/{code}.

EVENT coupon.redeemed

Coupon redeemed

A coupon was redeemed — through the API, or in the merchant center. data.redeemed_at says when.

EVENT coupon.cancelled

Coupon cancelled

The customer gave an unused coupon back. It can no longer be redeemed, and its stock returns to the deal.

EVENT deal.approved

Deal approved

DealNear approved one of your deals after review. data is the deal, as returned by GET /deals/{deal}; it is live from starts_at.

EVENT deal.rejected

Deal rejected

DealNear rejected one of your deals in review. data.rejection_reason explains why; edit the deal to send it back for review.

EVENT ping

Test event

Sent when you click “Send test event” in the merchant center, whatever events the endpoint subscribes to. It is never retried.