Working API for subscribing and unsubscribing
This commit is contained in:
parent
d5222f7b4d
commit
11f412ded1
15 changed files with 439 additions and 24 deletions
2
app.js
2
app.js
|
@ -30,6 +30,7 @@ let segments = require('./routes/segments');
|
|||
let webhooks = require('./routes/webhooks');
|
||||
let subscription = require('./routes/subscription');
|
||||
let archive = require('./routes/archive');
|
||||
let api = require('./routes/api');
|
||||
|
||||
let app = express();
|
||||
|
||||
|
@ -172,6 +173,7 @@ app.use('/segments', segments);
|
|||
app.use('/webhooks', webhooks);
|
||||
app.use('/subscription', subscription);
|
||||
app.use('/archive', archive);
|
||||
app.use('/api', api);
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use((req, res, next) => {
|
||||
|
|
|
@ -348,7 +348,7 @@ function addCustomField(listId, name, defaultValue, type, group, visible, callba
|
|||
});
|
||||
}
|
||||
|
||||
module.exports.getRow = (fieldList, values, useDate, showAll) => {
|
||||
module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
||||
let valueList = {};
|
||||
let row = [];
|
||||
|
||||
|
@ -363,6 +363,10 @@ module.exports.getRow = (fieldList, values, useDate, showAll) => {
|
|||
});
|
||||
|
||||
fieldList.filter(field => showAll || field.visible).forEach(field => {
|
||||
if (onlyExisting && field.column && !valueList.hasOwnProperty(field.column)) {
|
||||
// ignore missing values
|
||||
return;
|
||||
}
|
||||
switch (field.type) {
|
||||
case 'text':
|
||||
case 'website':
|
||||
|
@ -409,15 +413,21 @@ module.exports.getRow = (fieldList, values, useDate, showAll) => {
|
|||
mergeTag: field.key,
|
||||
mergeValue: field.defaultValue,
|
||||
['type' + (field.type || '').toString().trim().replace(/(?:^|\-)([a-z])/g, (m, c) => c.toUpperCase())]: true,
|
||||
options: (field.options || []).map(subField => ({
|
||||
type: subField.type,
|
||||
name: subField.name,
|
||||
column: subField.column,
|
||||
value: valueList[subField.column] ? 1 : 0,
|
||||
visible: !!subField.visible,
|
||||
mergeTag: subField.key,
|
||||
mergeValue: valueList[subField.column] ? subField.name : subField.defaultValue
|
||||
}))
|
||||
options: (field.options || []).map(subField => {
|
||||
if (onlyExisting && subField.column && !valueList.hasOwnProperty(subField.column)) {
|
||||
// ignore missing values
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
type: subField.type,
|
||||
name: subField.name,
|
||||
column: subField.column,
|
||||
value: valueList[subField.column] ? 1 : 0,
|
||||
visible: !!subField.visible,
|
||||
mergeTag: subField.key,
|
||||
mergeValue: valueList[subField.column] ? subField.name : subField.defaultValue
|
||||
};
|
||||
}).filter(subField => subField)
|
||||
};
|
||||
item.value = item.options.filter(subField => showAll || subField.visible && subField.value).map(subField => subField.name).join(', ');
|
||||
item.mergeValue = item.value || field.defaultValue;
|
||||
|
|
|
@ -248,7 +248,7 @@ module.exports.insert = (listId, meta, subscription, callback) => {
|
|||
}
|
||||
});
|
||||
|
||||
fields.getValues(fields.getRow(fieldList, subscription, true, true), true).forEach(field => {
|
||||
fields.getValues(fields.getRow(fieldList, subscription, true, true, !!meta.partial), true).forEach(field => {
|
||||
keys.push(field.key);
|
||||
values.push(field.value);
|
||||
});
|
||||
|
|
|
@ -21,7 +21,30 @@ module.exports.get = (id, callback) => {
|
|||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('SELECT id, username, email FROM users WHERE id=? LIMIT 1', [id], (err, rows) => {
|
||||
connection.query('SELECT `id`, `username`, `email`, `access_token` FROM `users` WHERE `id`=? LIMIT 1', [id], (err, rows) => {
|
||||
connection.release();
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let user = tools.convertKeys(rows[0]);
|
||||
return callback(null, user);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.findByAccessToken = (accessToken, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT `id`, `username`, `email`, `access_token` FROM `users` WHERE `access_token`=? LIMIT 1', [accessToken], (err, rows) => {
|
||||
connection.release();
|
||||
|
||||
if (err) {
|
||||
|
@ -48,7 +71,7 @@ module.exports.get = (id, callback) => {
|
|||
module.exports.authenticate = (username, password, callback) => {
|
||||
|
||||
let login = (connection, callback) => {
|
||||
connection.query('SELECT id, password FROM users WHERE username=? OR email=? LIMIT 1', [username, username], (err, rows) => {
|
||||
connection.query('SELECT `id`, `password`, `access_token` FROM `users` WHERE `username`=? OR email=? LIMIT 1', [username, username], (err, rows) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
@ -175,6 +198,34 @@ module.exports.update = (id, updates, callback) => {
|
|||
});
|
||||
};
|
||||
|
||||
module.exports.resetToken = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (!id) {
|
||||
return callback(new Error('User ID not set'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let token = crypto.randomBytes(20).toString('hex').toLowerCase();
|
||||
let query = 'UPDATE users SET `access_token`=? WHERE id=? LIMIT 1';
|
||||
let values = [token, id];
|
||||
|
||||
connection.query(query, values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result.affectedRows);
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
module.exports.sendReset = (username, callback) => {
|
||||
username = (username || '').toString().trim();
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"schemaVersion": 8
|
||||
"schemaVersion": 9
|
||||
}
|
||||
|
|
|
@ -72,7 +72,6 @@ $('.data-table-ajax').each(function () {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
$('.datestring').each(function () {
|
||||
$(this).html(moment($(this).data('date')).fromNow());
|
||||
});
|
||||
|
@ -126,6 +125,10 @@ $('.page-refresh').each(function () {
|
|||
}, interval * 1000);
|
||||
});
|
||||
|
||||
$('.click-select').on('click', function () {
|
||||
$(this).select();
|
||||
});
|
||||
|
||||
if (typeof moment.tz !== 'undefined') {
|
||||
(function () {
|
||||
var tz = moment.tz.guess();
|
||||
|
|
188
routes/api.js
Normal file
188
routes/api.js
Normal file
|
@ -0,0 +1,188 @@
|
|||
'use strict';
|
||||
|
||||
let users = require('../lib/models/users');
|
||||
let lists = require('../lib/models/lists');
|
||||
let fields = require('../lib/models/fields');
|
||||
let subscriptions = require('../lib/models/subscriptions');
|
||||
let tools = require('../lib/tools');
|
||||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.query.access_token) {
|
||||
res.status(403);
|
||||
return res.json({
|
||||
error: 'Missing access_token',
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
users.findByAccessToken(req.query.access_token, (err, user) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
if (!user) {
|
||||
res.status(403);
|
||||
return res.json({
|
||||
error: 'Invalid or expired access_token',
|
||||
data: []
|
||||
});
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
router.post('/subscribe/:listId', (req, res) => {
|
||||
let input = {};
|
||||
Object.keys(req.body).forEach(key => {
|
||||
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
|
||||
});
|
||||
lists.getByCid(req.params.listId, (err, list) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
if (!list) {
|
||||
res.status(404);
|
||||
return res.json({
|
||||
error: 'Selected listId not found',
|
||||
data: []
|
||||
});
|
||||
}
|
||||
if (!input.EMAIL) {
|
||||
res.status(400);
|
||||
return res.json({
|
||||
error: 'Missing EMAIL',
|
||||
data: []
|
||||
});
|
||||
}
|
||||
tools.validateEmail(input.EMAIL, false, err => {
|
||||
if (err) {
|
||||
res.status(400);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
let subscription = {
|
||||
email: input.EMAIL
|
||||
};
|
||||
|
||||
if (input.FIRST_NAME) {
|
||||
subscription.first_name = (input.FIRST_NAME || '').toString().trim();
|
||||
}
|
||||
|
||||
if (input.LAST_NAME) {
|
||||
subscription.last_name = (input.LAST_NAME || '').toString().trim();
|
||||
}
|
||||
|
||||
if (input.TIMEZONE) {
|
||||
subscription.tz = (input.TIMEZONE || '').toString().trim();
|
||||
}
|
||||
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
if (err && !fieldList) {
|
||||
fieldList = [];
|
||||
}
|
||||
|
||||
fieldList.forEach(field => {
|
||||
if (input.hasOwnProperty(field.key) && field.column) {
|
||||
subscription[field.column] = input[field.key];
|
||||
} else if (field.options) {
|
||||
for (let i = 0, len = field.options.length; i < len; i++) {
|
||||
if (input.hasOwnProperty(field.options[i].key) && field.options[i].column) {
|
||||
let value = input[field.options[i].key];
|
||||
if (field.options[i].type === 'option') {
|
||||
value = ['false', 'no', '0', ''].indexOf((value || '').toString().trim().toLowerCase()) >= 0 ? '' : '1';
|
||||
}
|
||||
subscription[field.options[i].column] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let meta = {
|
||||
partial: true
|
||||
};
|
||||
|
||||
if (input.FORCE_SUBSCRIBE === 'yes') {
|
||||
meta.status = 1;
|
||||
}
|
||||
|
||||
subscriptions.insert(list.id, meta, subscription, (err, response) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
res.status(200);
|
||||
res.json({
|
||||
data: {
|
||||
id: response.entryId,
|
||||
subscribed: true
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/unsubscribe/:listId', (req, res) => {
|
||||
let input = {};
|
||||
Object.keys(req.body).forEach(key => {
|
||||
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
|
||||
});
|
||||
lists.getByCid(req.params.listId, (err, list) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
if (!list) {
|
||||
res.status(404);
|
||||
return res.json({
|
||||
error: 'Selected listId not found',
|
||||
data: []
|
||||
});
|
||||
}
|
||||
if (!input.EMAIL) {
|
||||
res.status(400);
|
||||
return res.json({
|
||||
error: 'Missing EMAIL',
|
||||
data: []
|
||||
});
|
||||
}
|
||||
subscriptions.unsubscribe(list.id, input.EMAIL, false, (err, subscription) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
res.status(200);
|
||||
res.json({
|
||||
data: {
|
||||
id: subscription.id,
|
||||
unsubscribed: true
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -69,6 +69,10 @@ router.post('/update', passport.parseForm, passport.csrfProtection, (req, res) =
|
|||
Object.keys(data).forEach(key => {
|
||||
let value = data[key].trim();
|
||||
key = tools.toDbKey(key);
|
||||
// ensure trailing slash for service home page
|
||||
if (key === 'service_url' && value && !/\/$/.test(value)) {
|
||||
value = value + '/';
|
||||
}
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
|
|
|
@ -4,6 +4,7 @@ let passport = require('../lib/passport');
|
|||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
let users = require('../lib/models/users');
|
||||
let settings = require('../lib/models/settings');
|
||||
|
||||
router.get('/logout', (req, res) => passport.logout(req, res));
|
||||
|
||||
|
@ -67,6 +68,47 @@ router.post('/reset', passport.parseForm, passport.csrfProtection, (req, res) =>
|
|||
});
|
||||
});
|
||||
|
||||
router.all('/api', (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.flash('danger', 'Need to be logged in to access restricted content');
|
||||
return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl));
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/api', passport.csrfProtection, (req, res, next) => {
|
||||
users.get(req.user.id, (err, user) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
if (!user) {
|
||||
return next(new Error('User data not found'));
|
||||
}
|
||||
settings.list(['serviceUrl'], (err, configItems) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
user.serviceUrl = configItems.serviceUrl;
|
||||
user.csrfToken = req.csrfToken();
|
||||
res.render('users/api', user);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
router.post('/api/reset-token', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
users.resetToken(Number(req.user.id), (err, success) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
} else if (success) {
|
||||
req.flash('success', 'Access token updated');
|
||||
} else {
|
||||
req.flash('info', 'Access token not updated');
|
||||
}
|
||||
return res.redirect('/users/api');
|
||||
});
|
||||
});
|
||||
|
||||
router.all('/account', (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.flash('danger', 'Need to be logged in to access restricted content');
|
||||
|
|
|
@ -165,7 +165,8 @@ function processImport(data, callback) {
|
|||
|
||||
subscriptions.insert(listId, {
|
||||
imported: data.id,
|
||||
status: data.type
|
||||
status: data.type,
|
||||
partial: true
|
||||
}, entry, (err, response) => {
|
||||
if (err) {
|
||||
// ignore
|
||||
|
|
12
setup/sql/upgrade-00009.sql
Normal file
12
setup/sql/upgrade-00009.sql
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Header section
|
||||
# Define incrementing schema version number
|
||||
SET @schema_version = '9';
|
||||
|
||||
# Adds a column for static access tokens to be used in API authentication
|
||||
ALTER TABLE `users` ADD COLUMN `access_token` varchar(40) NULL DEFAULT NULL AFTER `email`;
|
||||
CREATE INDEX token_index ON `users` (`access_token`);
|
||||
|
||||
# Footer section
|
||||
LOCK TABLES `settings` WRITE;
|
||||
INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
|
||||
UNLOCK TABLES;
|
|
@ -167,7 +167,7 @@
|
|||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="id" value="{{id}}" />
|
||||
|
||||
<button type="submit" class="btn btn-danger"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Cancel</a>
|
||||
<button type="submit" class="btn btn-danger"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Cancel
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -178,7 +178,7 @@
|
|||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="id" value="{{id}}" />
|
||||
|
||||
<button type="submit" class="btn btn-info"><span class="glyphicon glyphicon-pause" aria-hidden="true"></span> Pause</a>
|
||||
<button type="submit" class="btn btn-info"><span class="glyphicon glyphicon-pause" aria-hidden="true"></span> Pause
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -192,7 +192,7 @@
|
|||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="id" value="{{id}}" />
|
||||
|
||||
<button type="submit" class="btn btn-info"><span class="glyphicon glyphicon-play" aria-hidden="true"></span> Resume</a>
|
||||
<button type="submit" class="btn btn-info"><span class="glyphicon glyphicon-play" aria-hidden="true"></span> Resume
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -211,10 +211,10 @@
|
|||
<input type="hidden" name="id" value="{{id}}" />
|
||||
</form>
|
||||
|
||||
<button type="submit" form="continue-sending" class="btn btn-info"><span class="glyphicon glyphicon-play" aria-hidden="true"></span> Continue</a>
|
||||
<button type="submit" form="continue-sending" class="btn btn-info"><span class="glyphicon glyphicon-play" aria-hidden="true"></span> Continue
|
||||
</button>
|
||||
|
||||
<button type="submit" form="reset-sending" class="btn btn-danger"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Reset</a>
|
||||
<button type="submit" form="reset-sending" class="btn btn-danger"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Reset
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
@ -236,7 +236,7 @@
|
|||
<input type="hidden" name="id" value="{{id}}" />
|
||||
</form>
|
||||
|
||||
<button type="submit" form="inactivate-sending" class="btn btn-warning"><span class="glyphicon glyphicon-pause" aria-hidden="true"></span> Pause</a>
|
||||
<button type="submit" form="inactivate-sending" class="btn btn-warning"><span class="glyphicon glyphicon-pause" aria-hidden="true"></span> Pause
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
@ -248,7 +248,7 @@
|
|||
<input type="hidden" name="id" value="{{id}}" />
|
||||
</form>
|
||||
|
||||
<button type="submit" form="activate-sending" class="btn btn-info"><span class="glyphicon glyphicon-play" aria-hidden="true"></span> Activate</a>
|
||||
<button type="submit" form="activate-sending" class="btn btn-info"><span class="glyphicon glyphicon-play" aria-hidden="true"></span> Activate
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -72,6 +72,11 @@
|
|||
<span class="glyphicon glyphicon-cog" aria-hidden="true"></span> Settings
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/users/api">
|
||||
<span class="glyphicon glyphicon-retweet" aria-hidden="true"></span> API
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/users/logout">
|
||||
<span class="glyphicon glyphicon-log-out" aria-hidden="true"></span> Log out
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<hr>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover data-table display nowrap" width="100%" data-row-sort="0,1,1,0,0">
|
||||
<table class="table table-bordered table-hover data-table display nowrap" width="100%" data-row-sort="0,1,1,1,0,0">
|
||||
<thead>
|
||||
<th class="col-md-1">
|
||||
#
|
||||
|
@ -20,6 +20,9 @@
|
|||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th class="col-md-2">
|
||||
ID
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
Subscribers
|
||||
</th>
|
||||
|
@ -44,6 +47,9 @@
|
|||
{{name}}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<input class="click-select gpg-text" type="text" readonly value="{{cid}}">
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-center">{{subscribers}}</p>
|
||||
</td>
|
||||
|
|
91
views/users/api.hbs
Normal file
91
views/users/api.hbs
Normal file
|
@ -0,0 +1,91 @@
|
|||
<ol class="breadcrumb">
|
||||
<li><a href="/">Home</a></li>
|
||||
<li class="active">API</li>
|
||||
</ol>
|
||||
|
||||
<h2>API</h2>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<div class="pull-right">
|
||||
<form class="form-horizontal confirm-submit" {{#if accessToken}} data-confirm-message="Are you sure? Resetting would invalidate the currently existing token." {{else}} data-confirm-message="Are you sure?" {{/if}} method="post" action="/users/api/reset-token">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<button type="submit" class="btn btn-info"><span class="glyphicon glyphicon-retweet" aria-hidden="true"></span>
|
||||
{{#if accessToken}}
|
||||
Reset Access Token
|
||||
{{else}}
|
||||
Generate Access Token
|
||||
{{/if}}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{{#if accessToken}}
|
||||
Personal access token: <code>{{accessToken}}</code>
|
||||
{{else}}
|
||||
Access token not yet generated
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>POST /api/subscribe/:listId – Add subscription</h3>
|
||||
|
||||
<p>
|
||||
This API call either inserts a new subscription or updates existing. Fields not included are left as is, so if you update only LAST_NAME value, then FIRST_NAME is kept untouched for an existing subscription.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>GET</strong> arguments
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>access_token</strong> – your personal access token
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>POST</strong> arguments
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>EMAIL</strong> – subscriber's email address (<em>required</em>)
|
||||
<li><strong>FIRST_NAME</strong> – subscriber's first name
|
||||
<li><strong>LAST_NAME</strong> – subscriber's last name
|
||||
<li><strong>TIMEZONE</strong> – subscriber's timezone (eg. "Europe/Tallinn", "PST" or "UTC"). If not set defaults to "UTC"
|
||||
<li><strong>MERGE_TAG_VALUE</strong> – custom field value. Use yes/no for option group values (checkboxes, radios, drop downs)
|
||||
<li>
|
||||
<strong>FORCE_SUBSCRIBE</strong> – set to "yes" if you want to make sure the email is marked as subscribed even if it was previously marked as unsubscribed. By default if the email was already unsubscribed then subscription status is not changed.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>Example</strong>
|
||||
</p>
|
||||
|
||||
<pre>curl -XPOST {{serviceUrl}}api/subscribe/B16uVTdW?access_token={{accessToken}}\
|
||||
--data 'EMAIL=test@example.com&MERGE_CHECKBOX=yes'</pre>
|
||||
|
||||
<h3>POST /api/unsubscribe/:listId – Remove subscription</h3>
|
||||
|
||||
<p>
|
||||
This API call marks a subscription as unsubscribed
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>GET</strong> arguments
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>access_token</strong> – your personal access token
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>POST</strong> arguments
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>EMAIL</strong> – subscriber's email address (<em>required</em>)
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>Example</strong>
|
||||
</p>
|
||||
|
||||
<pre>curl -XPOST {{serviceUrl}}api/unsubscribe/B16uVTdW?access_token={{accessToken}}\
|
||||
--data 'EMAIL=test@example.com'</pre>
|
Loading…
Reference in a new issue