API documentation

With Giift's API you connect your own systems: your website, your accounting, or Zapier and Make. Read and change contacts, record consent, manage campaigns, and get webhooks when something changes.

Getting started

  1. Create an API key in Account → API. Only an organisation's owner and admins can.
  2. Choose what the key may do: everything, read only, or forms only (for Miix Donate in WordPress).
  3. Send the key in the header Authorization: Bearer giift_sk_… with every request to https://api.giift.nl/v1.
Your first request
curl https://api.giift.nl/v1/contacts?limit=10 \
  -H "Authorization: Bearer $GIIFT_API_KEY"

A key gives access to your donors' data. Keep it on your server, never in a web page's or app's code, and revoke it in Account → API if it leaks.

Keys and scopes

Every key belongs to one organisation and has a scope:

ScopeCan
fullEverything: read and change contacts, campaigns and webhooks.
readRead only: fetch contacts, timelines, campaigns and webhooks.
checkoutOnly what a donation form needs: forms, gifts through a form and campaign totals. For Miix Donate in WordPress.

Everything you do with a contact through the API (view, change, delete) goes into the organisation's log, under the key's name. A request the scope doesn't allow gets 403 forbidden.

Conventions

  • Requests and responses are JSON; send Content-Type: application/json.
  • Amounts are always whole cents (goalCents: 500000 is €5,000), in euro.
  • Dates and times are ISO 8601 with a time zone, e.g. 2026-12-31T23:59:00+01:00. Responses are in UTC.
  • A list returns data, page, limit, totalPages and totalCount. Page with ?page=2&limit=100 (at most 100).
  • Each key may make 120 requests a minute. Beyond that you get 429 with a Retry-After header in seconds.
  • Within /v1 things are only added: new fields, endpoints and events. Ignore fields you don't know.
A list
{
  "data": [ { "id": 4821, "firstName": "Anna", "...": "..." } ],
  "page": 1,
  "limit": 50,
  "totalPages": 3,
  "totalCount": 128
}

Errors

An error always has the same shape: a code for your code to act on, a message for people and sometimes details per field.

422 invalid_request
{
  "error": {
    "code": "invalid_request",
    "message": "The contact is invalid. See details.",
    "details": [
      { "path": "email", "message": "invalid_email" },
      { "path": "birthday", "message": "invalid_date" }
    ]
  }
}
StatusCodeMeaning
400/422invalid_requestThe request isn't right; see details.
401unauthorizedNo key, or an invalid one.
403forbiddenThe key's scope doesn't allow this.
403account_blockedThe organisation is blocked; please contact us.
404not_foundDoesn't exist, or belongs to another organisation.
409conflictClashes with something that exists, like an email address.
429rate_limitedToo many requests; wait Retry-After seconds.
500server_errorSomething went wrong on our side. Please try again later.

Contacts

A contact is a person or an organisation. A person can be a contact person of an organisation contact (organisationContactId, with jobTitle).

A contact
{
  "data": {
    "id": 4821,
    "kind": "person",
    "firstName": "Anna",
    "lastName": "de Vries",
    "organisationName": null,
    "jobTitle": null,
    "email": "anna@example.nl",
    "phone": "06 12345678",
    "birthday": "1980-03-31",
    "language": "nl",
    "street": "Dorpsstraat",
    "houseNumber": "12",
    "houseNumberAddition": "a",
    "postcode": "6811 AB",
    "city": "Arnhem",
    "country": "NL",
    "organisationContactId": null,
    "status": "active",
    "source": "api",
    "tags": ["Vrijwilliger"],
    "consents": {
      "email": null,
      "newsletter": { "granted": true, "at": "2026-10-18T09:12:00.000Z", "source": "api", "text": "Ja, ik wil de nieuwsbrief" },
      "phone": null,
      "post": null
    },
    "giving": { "totalCents": 0, "count": 0, "firstAt": null, "lastAt": null },
    "deceasedAt": null,
    "createdAt": "2026-10-18T09:12:00.000Z",
    "updatedAt": "2026-10-18T09:12:00.000Z"
  }
}

Listing contacts

GET/v1/contacts

ParameterMeaning
qSearches name, email, phone, city and postcode.
emailExactly this email address (case doesn't matter).
tagContacts with this tag (its name).
consentWith consent for email, newsletter, phone or post.
kindperson or organisation.
statusActive and deceased by default; deceased for deceased contacts only.
updatedSinceChanged since this moment: to keep another system up to date.
sortname (default), newest or updated.
page, limitPaging; limit is at most 100.

Contacts in the list come without consents; you get those with a single contact.

GET/v1/contacts/{id}

Creating a contact

POST/v1/contacts

A person needs a first name, last name or email address; an organisation (kind: "organisation") an organisationName. Postcodes are written neatly (6811 AB), email addresses in lower case. With tags (names; new ones are created) and consents you record consent straight away, with the wording it was given with in consentText.

curl
curl https://api.giift.nl/v1/contacts \
  -H "Authorization: Bearer $GIIFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Anna",
    "lastName": "de Vries",
    "email": "anna@example.nl",
    "postcode": "6811AB",
    "houseNumber": "12",
    "tags": ["Vrijwilliger"],
    "consents": { "newsletter": true },
    "consentText": "Ja, ik wil de nieuwsbrief"
  }'

If a contact with the email address exists, you get 409 conflict with its id in the message. So look it up first:

JavaScript: find or create
const headers = {
  Authorization: `Bearer ${process.env.GIIFT_API_KEY}`,
  "Content-Type": "application/json",
};
const email = "anna@example.nl";

// Find by email address first; create the contact when there is none.
const found = await fetch(`https://api.giift.nl/v1/contacts?email=${encodeURIComponent(email)}`, { headers });
const { data } = await found.json();
const contact = data[0] ?? (await (await fetch("https://api.giift.nl/v1/contacts", {
  method: "POST",
  headers,
  body: JSON.stringify({ firstName: "Anna", lastName: "de Vries", email }),
})).json()).data;

Changing a contact

PATCH/v1/contacts/{id}

Only the fields you send change; null empties a field. tags is the full list: tags not in it are removed. With status you mark a contact deceased or active again.

curl
curl -X PATCH https://api.giift.nl/v1/contacts/4821 \
  -H "Authorization: Bearer $GIIFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "city": "Nijmegen", "phone": null, "tags": ["Vrijwilliger", "Gala 2026"] }'

Merging

POST/v1/contacts/{id}/merge

With { "mergeId": 4822 } contact 4822 is merged into this one: empty fields are filled in, tags, consents and the timeline move over, and 4822 is deleted.

Deleting

DELETE/v1/contacts/{id}

For a request to be forgotten: the personal data, consents, tags and timeline are deleted. Gift amounts stay, without a name, for the books. The contact returns 404 afterwards.

Consent, notes and timeline

PUT/v1/contacts/{id}/consents

Consent is per channel (email, newsletter, phone, post): true is given, false withdrawn. Channels you don't send stay as they are. Giift keeps every change with its source, date and the text the person saw; at sets an earlier moment.

curl
curl -X PUT https://api.giift.nl/v1/contacts/4821/consents \
  -H "Authorization: Bearer $GIIFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "newsletter": false, "text": "Afgemeld via onze website" }'

POST/v1/contacts/{id}/notes

A note (note), phone call (call), email (email) or meeting (meeting) on the timeline.

curl
curl https://api.giift.nl/v1/contacts/4821/notes \
  -H "Authorization: Bearer $GIIFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "type": "call", "body": "Bedankt voor de gift; wil graag het jaarverslag." }'

GET/v1/contacts/{id}/timeline

Everything that happened with the contact, newest first: notes, changes, consent, imports and merges. by is who did it.

Campaigns

A campaign is what a gift is for: a cause with a name, a description for the thank-you email, a goal and a period. Its id starts with cmp_.

GET/v1/campaigns

POST/v1/campaigns

curl
curl https://api.giift.nl/v1/campaigns \
  -H "Authorization: Bearer $GIIFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Noodhulp honden",
    "description": "Voor honden in nood.",
    "goalCents": 500000,
    "endsAt": "2026-12-31T23:59:00+01:00",
    "publicTotal": true
  }'

GET/v1/campaigns/{id}

PATCH/v1/campaigns/{id}

Changes the fields you send. archived: true puts a campaign away; list archived ones with ?archived=true.

Showing what's been raised

GET/v1/campaigns/{id}/totals

Raised, number of donors and progress. Without a key, from any website, for campaigns with publicTotal: true; cached for a minute. The amounts grow as gifts come in through Giift.

On your website
<p id="raised">…</p>
<script>
  fetch("https://api.giift.nl/v1/campaigns/cmp_k7Qx2mPa9rTb/totals")
    .then((response) => response.json())
    .then(({ data }) => {
      const euros = new Intl.NumberFormat("nl-NL", { style: "currency", currency: "EUR" });
      document.getElementById("raised").textContent =
        `${euros.format(data.raisedCents / 100)} van ${euros.format(data.goalCents / 100)}`;
    });
</script>

Webhooks

With a webhook Giift sends a message to your system as soon as something happens. Add one in Account → API or through the API.

GET/v1/webhooks

POST/v1/webhooks

curl
curl https://api.giift.nl/v1/webhooks \
  -H "Authorization: Bearer $GIIFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.org/hooks/giift",
    "events": ["contact.created", "contact.updated", "contact.deleted"]
  }'

The response holds the secret you use to check that messages come from Giift; through the API you only see it then. The URL must be https and publicly reachable.

DELETE/v1/webhooks/{id}

Events

EventWhen
contact.createdA contact was created, in Giift, by an import or through the API.
contact.updatedDetails, tags, consent or status changed.
contact.deletedA contact was deleted (anonymised) or merged; then mergedInto is included.
donation.paidA gift was paid. Comes with gifts in Giift.
donation.failedA gift or direct debit failed. Comes with gifts in Giift.
donation.refundedA gift was refunded or charged back. Comes with gifts in Giift.
pledge.createdA recurring gift started. Comes with recurring gifts.
pledge.stoppedA recurring gift stopped. Comes with recurring gifts.
A message
{
  "id": "evt_Qm3xT9a2LpZk",
  "type": "contact.updated",
  "createdAt": "2026-10-18T09:15:42.000Z",
  "data": {
    "contact": { "id": 4821, "firstName": "Anna", "...": "..." }
  }
}
  • Giift sends a POST with the headers Giift-Event, Giift-Delivery and Giift-Signature.
  • Answer within 10 seconds with a 2xx. Otherwise Giift tries again, waiting longer each time, for about two days.
  • The same message can arrive more than once: use id to spot duplicates. One change can send several contact.updated messages; each holds the latest state.
  • Giift switches off a webhook that keeps failing; switch it back on in Account → API.

Checking the signature

Giift-Signature is t=<timestamp>,v1=<signature>: an HMAC-SHA256 of the timestamp, a dot and the body, with your webhook secret. Refuse messages with another signature or a timestamp more than five minutes old.

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

// In your route: read the body as text, exactly as it arrived.
export function isFromGiift(body, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (!parts.v1 || age > 300) return false; // older than 5 minutes: refuse
  const expected = createHmac("sha256", secret).update(`${parts.t}.${body}`).digest("hex");
  return expected.length === parts.v1.length &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

// isFromGiift(rawBody, request.headers["giift-signature"], process.env.GIIFT_WEBHOOK_SECRET)

Privacy

  • Your organisation is responsible for its contacts' data; Giift processes it for you. Only fetch what you need.
  • Every view, change and deletion through the API is in the log at Account → Privacy and log.
  • Webhook messages hold personal data: make sure the receiver stores them as carefully. Giift deletes sent messages after 30 days.
  • Handle an access request or a request to be forgotten with GET /v1/contacts/{id} and DELETE /v1/contacts/{id}.

OpenAPI

The full description of the API is at https://api.giift.nl/v1/openapi.json as OpenAPI 3.1, for tools like Postman and Insomnia and to generate code.

Changes

DateChange
2026-10-18First version: keys with scopes, contacts, consent, notes, timeline, campaigns and webhooks.