DealNear Merchant API v1
API 文件目前提供英文版。
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".
| Scope | Allows |
|---|---|
| deals:read | 查看優惠列表和詳情 |
| deals:write | 建立、編輯、下架和重新上架優惠 |
| coupons:read | 查看顧客已領取的優惠券 |
| coupons:redeem | 在收銀時核銷優惠券 |
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:
| Status | error | Meaning |
|---|---|---|
| 409 | already_redeemed | Used before — see data.redeemed_at. |
| 409 | coupon_cancelled | The customer gave the coupon back. |
| 409 | coupon_expired | The deal has ended. |
| 404 | not_found | No 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
2xxstatus 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
idyou have already handled, and comparedata.updated_atbefore 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:
- Split the
DealNear-Signatureheader on,intot(a Unix timestamp) andv1. - Compute HMAC-SHA256 of
{t}.{raw request body}with your secret, as lowercase hex. Use the body exactly as received, before any JSON parsing. - Compare it with
v1in 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.
| Status | When |
|---|---|
| 401 | Missing, invalid or revoked API key. |
| 403 | The key lacks the permission (missing_scope), or your business is suspended.
|
| 404 | Not found, or it belongs to another business (not_found).
|
| 409 | The action conflicts with the current state, e.g. redeeming a used coupon or pausing a pending deal (invalid_status).
|
| 422 | Validation failed. errors lists the messages per field, e.g. "errors": {"content.en.title": ["The Title (English) field is required."]}.
|
| 429 | Too many requests (rate_limited). Wait for the number of seconds in the Retry-After header.
|
Pagination & limits
- Lists return
data,linksandmeta. Useper_page(up to 100) and followlinks.nextuntil it isnull. - 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-LimitandX-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.
/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.
Coupons
Coupons customers have claimed from your deals.
/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. |
/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.
|
/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.
|
Deals
The offers you publish on DealNear.
/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. |
/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.
/deals/{deal}
deals:read
Get a deal
| deal | path, required | Deal ID. |
/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.
/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. |
/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. |
Reference
IDs used when creating deals.
/categories
any key
List categories
/cities
any key
List cities
Webhook events
Requests DealNear sends to your endpoints when coupons and deals change. Every request has the headers and envelope shown in Webhooks.
coupon.claimed
Coupon claimed
A customer claimed a coupon from one of your deals. data is the coupon, as returned by GET /coupons/{code}.
coupon.redeemed
Coupon redeemed
A coupon was redeemed — through the API, or in the merchant center. data.redeemed_at says when.
coupon.cancelled
Coupon cancelled
The customer gave an unused coupon back. It can no longer be redeemed, and its stock returns to the deal.
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.
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.
ping
Test event
Sent when you click “Send test event” in the merchant center, whatever events the endpoint subscribes to. It is never retried.