Sign in Create account
Guides

CRM connections: HubSpot, Pipedrive and RD Station CRM

Keep Postal contacts and your CRM in sync, both ways and without loops: HubSpot, Pipedrive and RD Station CRM, with field and stage mapping, a sync log and retries.

#Overview

A CRM connection keeps Postal contacts and the people in your CRM in sync, both ways and without loops. What a person does in the CRM (a new contact, a deal that changes stage) reaches Postal as a contact, tag, property and event, and can start an automation. What happens in Postal (a contact created or changed, an email opened, clicked, replied to or bounced) goes back to the CRM as the person's data and as a note on their timeline.

ConnectionCredentialsFrom the CRM to PostalFrom Postal to the CRM
HubSpotPrivate app token (and, for webhooks, the client secret)Contact and deal webhooks, plus the manual syncContacts created or updated; notes on the timeline
PipedriveAPI token and account addressPerson and deal webhooks, plus the manual syncPersons created or updated; notes on the person
RD Station CRMInstance tokenContact and deal webhooks, plus the manual syncContacts created or updated; activities on the deal or a note on the contact
Webhook (other CRM)None: a signing secret generated by PostalAny system that sends a signed JSON POSTSigned POST to your system's HTTPS URL

The three native connections use the token you paste (no OAuth) and read the CRM's API for you. The webhook connection is for any CRM or system that sends and receives webhooks; it has its own page.

App integrations (Stripe, Supabase, Clerk, Auth0 and Firebase) use the same connections, but count separately, in app_integrations, and do not sync back.

Depends on your plan

The number of CRM connections comes from the marketing plan (crm_connections; 0 means not included). Going over the limit answers 403 plan_limit_reached. See Limits; GET /crm/providers shows the limit and how many you have already used. Connections, and the contacts they create, count toward plan limits like any others.

#Create a connection

In the dashboard, open Integrations › CRM, pick the CRM and paste the credentials. They are stored encrypted and never shown again: the API returns only the names of the stored fields. Use Test connection to check the access. Through the API, it is POST /crm-connections, with a full-access key.

POST /crm-connections

cURL

curl -X POST https://api.postal.so/crm-connections \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "hubspot",
    "name": "HubSpot",
    "credentials": {
      "access_token": "pat-na1-…",
      "client_secret": "…"
    },
    "settings": {
      "direction": "both"
    }
  }'

Node.js

const response = await fetch('https://api.postal.so/crm-connections', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    provider: 'hubspot',
    name: 'HubSpot',
    credentials: {
      access_token: 'pat-na1-…',
      client_secret: '…',
    },
    settings: {
      direction: 'both',
    },
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/crm-connections",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "provider": "hubspot",
        "name": "HubSpot",
        "credentials": {
            "access_token": "pat-na1-…",
            "client_secret": "…",
        },
        "settings": {
            "direction": "both",
        },
    },
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->post('crm-connections', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'json' => [
        'provider' => 'hubspot',
        'name' => 'HubSpot',
        'credentials' => [
            'access_token' => 'pat-na1-…',
            'client_secret' => '…',
        ],
        'settings' => [
            'direction' => 'both',
        ],
    ],
]);

$data = json_decode($response->getBody(), true);
201Response
{
  "object": "crm_connection",
  "id": "019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
  "provider": "hubspot",
  "name": "HubSpot",
  "status": "active",
  "settings": {
    "direction": "both",
    "field_mappings": [],
    "stage": null,
    "inbound": {
      "items_path": null,
      "event_field": null,
      "allow_token": false
    },
    "outbound": {
      "url": null,
      "events": [
        "contact.created",
        "contact.updated",
        "contact.deleted",
        "email.opened",
        "email.clicked",
        "email.replied",
        "email.bounced",
        "email.complained"
      ]
    },
    "skip_sources": []
  },
  "credentials": ["access_token", "client_secret"],
  "source": "crm:019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
  "webhook_url": "https://api.postal.so/integrations/crm/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/webhook",
  "webhook_secret": "whsec_c2VncmVkby1kZS1leGVtcGxvLTEyMzQ1Ng==",
  "last_sync_at": null,
  "last_error": null,
  "last_error_at": null,
  "created_at": "2026-10-26T14:00:00.000000Z",
  "updated_at": "2026-10-26T14:00:00.000000Z"
}
  • provider: hubspot, pipedrive, rdstation_crm or generic (the webhook). The credentials for each are below.
  • status starts as active. paused (PATCH) stops receiving and sending without deleting anything; error is only a warning that the last push failed for good, and goes back to active on the first push that works.
  • webhook_url is where the CRM delivers changes; webhook_secret (whsec_…) is the secret that authenticates the delivery. They appear on creation, when you get one connection and when you generate a new secret, not in the list.
  • source (crm:<id>) is the source of everything the connection changes on contacts.

#What syncs, and in which direction

settings.direction picks the direction: both (default), inbound (only from the CRM to Postal) or outbound (only from Postal to the CRM). On a send-only connection, the CRM's webhook answers 409 connection_paused; on a receive-only one, Postal sends nothing.

From the CRM to Postal

  • Each person becomes a contact, created or updated. Postal recognizes the contact by its link to the CRM record, then by external_id and finally by email; so an email change in the CRM updates the same contact.
  • Mapped fields become name, properties and tags (see Map fields and stages). People on the suppression list do not become contacts (the entry is logged as skipped).
  • Someone who unsubscribed is never resubscribed by a CRM: unsubscribed: false does not undo an unsubscribe (only the contacts API does, with proof of the new consent).
  • An event from the CRM, when the mapping names one (inbound.event_field), is recorded on the contact and can start an automation with the event trigger.
  • Deletions in the CRM do not delete the contact in Postal.

From Postal to the CRM

Postal sends the changes the CRM subscribed to in settings.outbound.events. The default is:

EventIn the CRMDefault
contact.created, contact.updatedThe person is created or updated (name, email, mapped fields).Yes
contact.deletedA note saying the contact was deleted in Postal. The CRM record is not deleted.Yes
email.opened, email.clicked, email.replied, email.bounced, email.complainedA note on the person's timeline, with the subject (and the link, the bounce reason…).Yes
email.sent, email.delivered, email.failed, email.suppressedThe same, for those who want the full history.No

The events are the same as Postal's webhooks. Any other name in outbound.events gives 422.

The text of the notes is always in Brazilian Portuguese, as the CRM's users read it (for example, “Postal: E-mail aberto”, “Assunto: …”). The names of the fields Postal writes to the CRM do not change with the language.

Sync now

The Sync now button (POST /crm-connections/{id}/sync) runs in the background: it reads the CRM's people, page by page (and, if there are stage mappings, the open deals), and then sends every Postal contact, as the direction allows. Each person appears in the log, and a sync entry summarizes how many people were imported and sent and how many failed. Use it for the first load, to recover what a webhook missed or when the CRM cannot send webhooks.

#Map fields and stages

With no mapping, Postal reads and writes its own format. On the native connections, email, first and last name and the record ID are already linked, and what you map is added to them. Each mapping links a CRM field to a Postal field, with its own direction (both, inbound or outbound):

Postal fieldWhat it is
emailThe contact's email.
external_idThe person's ID in your app (the same external_id as in contacts).
crm_idThe record ID in the CRM. Postal keeps it in the connection's link, never in external_id.
first_name, last_nameFirst and last name. An empty value in the CRM clears the field.
unsubscribedTrue or false. See the unsubscribe rule above.
tagsTags: a list, or text separated by commas or semicolons.
properties.<key>An existing contact property (properties.company). A key that does not exist gives 422.

The CRM field is a JSONPath-style path, up to 255 characters, with a small subset: $.person.email (or person.email), items[0].name, $.data.items[*] (every element of a list) and $ (the whole document). A path that does not exist reads null, and the field is ignored (except first and last name, which are left empty). On the native connections, paths point at the record Postal builds from the CRM (for example, $.properties.company on HubSpot and $.custom.<key> on Pipedrive and RD Station CRM); GET /crm-connections/{id}/fields lists the CRM fields you can choose from, with their type.

Pipeline stages

When a deal changes stage, its person gets a Postal property and/or tag, and you can start an automation on that change. In settings.stage, crm_field and pipeline_field say where the stage and the pipeline are (on the native connections, $.deal.stage and $.deal.pipeline are already the default), and each item in mappings links a stage (and, optionally, a pipeline) to a property_key with property_value, to a tag or to both. Names are compared case-insensitively. Without property_value, the property gets the stage name. When sending, Postal does not move deals in the CRM. GET /crm-connections/{id}/stages lists the stages of each pipeline.

Settings with mappings (PATCH /crm-connections/{id})
{
  "settings": {
    "direction": "both",
    "field_mappings": [
      {
        "crm_field": "$.properties.company",
        "postal_field": "properties.company",
        "direction": "both"
      },
      {
        "crm_field": "$.properties.hs_lead_status",
        "postal_field": "tags",
        "direction": "inbound"
      }
    ],
    "stage": {
      "crm_field": "$.deal.stage",
      "pipeline_field": "$.deal.pipeline",
      "mappings": [
        {
          "stage": "Closed won",
          "pipeline": "Sales",
          "property_key": "lifecycle",
          "property_value": "customer",
          "tag": "customer"
        }
      ]
    },
    "outbound": {
      "events": [
        "contact.created",
        "contact.updated",
        "email.opened",
        "email.clicked",
        "email.replied"
      ]
    },
    "skip_sources": [
      "my-app"
    ]
  }
}

In this example, HubSpot's company field goes back and forth to Postal's company property, the lead status only comes in as a tag, and a deal in “Closed won” of the “Sales” pipeline marks the person with lifecycle = customer and the customer tag. The company and lifecycle properties must exist beforehand. Only the listed events go out to the CRM, and changes coming from my-app do not go back to it.

#No loops

Two connections that feed each other could swap the same change forever. Postal prevents that in two ways:

  • Everything a connection changes on a contact carries the source crm:<id> (last_change_source), and that connection does not send back the change it made itself. The team's other connections receive the change as usual.
  • settings.skip_sources lists sources whose changes the connection does not send (up to 20; lowercase letters, numbers, ., _, : and -; a trailing * works as a prefix). Use it for the system that writes to both Postal and the CRM, for example your app, which sends contacts through the API with the source label my-app, and crm:* to ignore every CRM connection.

#Native connections

The three work the same way: Postal calls the CRM's API with your token (with a 10-second timeout, not following redirects), looks for the person by the stored link and then by email, and creates or updates the record. A CRM that answers 429 is waited for as long as it asks, and a 5xx is tried again. People without an email are ignored. The CRM's error message appears in the log and never carries the token.

To receive changes from the CRM you have to set up the webhook in the CRM itself, with the connection URL (webhook_url); without a webhook, the connection still sends data and reads the CRM when you ask for Sync now. Each CRM authenticates the webhook in its own way:

CRMHow Postal checks the webhook
HubSpotThe X-HubSpot-Signature-v3 signature, computed with the app's client secret. Without the client secret in the credentials, HubSpot does not send changes to Postal.
PipedriveHTTP basic authentication on the webhook: user postal and a password equal to the connection's webhook_secret.
RD Station CRMRD Station CRM does not sign webhooks: the URL carries ?token=<webhook_secret> (or the X-Postal-Token header). The token in the URL is the protection; do not share it.

A wrong signature or credential answers 401 invalid_signature. If the secret is compromised, generate another with POST /crm-connections/{id}/webhook-secret/rotate and update the webhook in the CRM: the old one stops working right away.

HubSpot

  • Credentials: access_token (required) and client_secret (optional, only for webhooks). In HubSpot, go to Settings › Integrations › Private Apps and create an app with permission for contacts (read and write), deals (read) and contact and deal schemas (read); copy the access token. The client secret is the one of the app that signs the notifications.
  • Webhook: in the app, subscribe to contacts (creation and property change) and deals (creation and stage change) with the connection URL. Deletions and merges are ignored.
  • Fields: the record is HubSpot's own, {"id", "properties": {…}}, so paths are $.properties.<internal name>. Postal reads the properties you map, plus email, first name, last name and phone. For deals, $.deal.stage, $.deal.pipeline, $.deal.status, $.deal.title and $.deal.value.
  • Sending: the contact is updated through the link, or looked up by email and created if it does not exist. Email activity becomes a note associated with the contact.

Pipedrive

  • Credentials: api_token and company_domain. In Pipedrive, open the account menu › Personal preferences › API and copy the personal token. The account address is only the first part of the address you use in the browser: mycompany in mycompany.pipedrive.com (lowercase letters, numbers and hyphens).
  • Webhook: under Tools and apps › Webhooks, create one webhook for persons and another for deals (all events), with the connection URL and the HTTP basic authentication above. Deletions are ignored. A change to a deal reads its person and the names of the stage and the pipeline.
  • Fields: $.email, $.first_name, $.last_name, $.name, $.phone, $.organization, $.id, the person's custom fields at $.custom.<key> and, for deals, $.deal.stage, $.deal.pipeline, $.deal.status, $.deal.title and $.deal.value.
  • Sending: the person is updated through the link or looked up by email and created if it does not exist; the new email and phone enter as the primary ones without deleting the others. Email activity becomes a note on the person.

RD Station CRM

  • Credentials: token, the instance token. In RD Station CRM, open the account menu › Settings › Integrations › API token.
  • Webhook: create one webhook for contacts and another for deals with the connection URL including the ?token= (the connection screen shows the ready URL). Deletions are ignored.
  • Fields: $.email, $.first_name, $.last_name, $.name, $.phone, $.title, $.organization, $.id, the contact's custom fields at $.custom.<id> and, for deals, $.deal.stage, $.deal.pipeline, $.deal.status, $.deal.title and $.deal.value.
  • Sending: the contact is updated through the link or looked up by email and created if it does not exist. Email activity becomes an activity on the contact's open deal; with no deal, a line in the contact's notes (the oldest drop out beyond 4,000 characters).

Token permissions

If the CRM refuses the token or a permission (HTTP 401 or 403), the log shows the message and the connection keeps its error warning until a push works. Check the token and the permissions and use Test connection.

#Sync log

Each webhook received, person synced, push to the CRM, test and full sync becomes a log entry, under Integrations › CRM (on the connection) and in GET /crm-connections/{id}/logs. An entry has:

FieldValues
directioninbound (from the CRM) or outbound (to the CRM). Filter: ?direction=.
object_typewebhook (the delivery received), person, activity (email activity), test or sync.
statuspending (queued or waiting for a retry), success, failed or skipped (nothing to do: suppressed contact, no match, paused connection). Filter: ?status=.
actionWhat happened: created, updated, unchanged, upserted, activity, deleted or a summary (“2 people”).
error, attempts, next_attempt_atThe message, how many attempts have happened and when the next one will be.
external_idThe record ID in the CRM, when known.
retryable, payloadWhether it can be sent again, and what was received or sent (cut at 16 KB; a cut payload cannot be retried).

The log messages are written in Brazilian Portuguese.

200Response
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "crm_sync_log",
      "id": "019a2c10-5e6f-7a81-9b2c-3d4e5f6a7b8c",
      "direction": "outbound",
      "object_type": "person",
      "event": "contact.updated",
      "external_id": "51",
      "contact_id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
      "action": null,
      "status": "failed",
      "error": "HubSpot recusou as credenciais ou a permissão (HTTP 403). Confira o token e as permissões dele.",
      "attempts": 6,
      "retryable": true,
      "payload": {
        "id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
        "email": "[email protected]",
        "first_name": "Jane"
      },
      "next_attempt_at": null,
      "created_at": "2026-10-26T14:05:10.000000Z",
      "updated_at": "2026-10-26T14:05:11.000000Z"
    }
  ]
}

Retries

  • Pushes to the CRM that fail are retried on their own, after 30 seconds, 2 minutes, 10 minutes, 30 minutes and 2 hours (6 attempts in all). Once they run out, the entry becomes failed and the connection shows the error warning (last_error); it keeps working and goes back to active on the first push that works. If the CRM answered 429, Postal waits at least as long as it asked.
  • Webhooks received are answered right away with 202 and processed in the background. If the CRM could not be queried (rate limit, down), Postal tries again, up to 3 times.
  • A failed entry with its full payload can be sent again by hand, in the dashboard or through POST /crm-connections/{id}/logs/{log_id}/retry, after fixing the cause (a missing email, a property that did not exist, the token).

#Through the API

Everything the dashboard does is in the API, with a full-access key (a sending-only key gets 401 restricted_api_key). IDs are UUIDs, and lists follow cursor pagination.

EndpointWhat it does
GET /crm/providersThe connectors (generic, pipedrive, rdstation_crm, hubspot and the app sources), the plan limit and the usage of each counter, and the partner platform that manages the team, if any.
GET /crm-connectionsLists the connections. ?limit_key=crm_connections (or app_integrations) filters by counter.
POST /crm-connectionsCreates one. Body: provider, name, credentials and settings.
GET /crm-connections/{id}One connection, with the webhook_secret.
PATCH /crm-connections/{id}Changes name, status (active or paused), credentials (they replace all: send every required one) and settings (they replace the previous ones: send the full set).
DELETE /crm-connections/{id}Deletes the connection and its log. The CRM stops syncing; the contacts stay.
POST /crm-connections/{id}/testChecks the access to the CRM (on native ones, a light call; on the webhook, a postal.test). Writes a test entry to the log.
POST /crm-connections/{id}/previewShows what a sample payload would change (payload, plus optional settings to try without saving), without saving anything. Built for the webhook connection.
GET /crm-connections/{id}/fieldsThe CRM fields for mapping (key, label, type). On native ones, they come from the CRM.
GET /crm-connections/{id}/stagesThe stages of each pipeline (pipeline, stage, and status when the CRM says). Empty on connectors without pipelines.
POST /crm-connections/{id}/syncQueues the sync (202). 422 if the connection is paused or the direction does not allow it.
POST /crm-connections/{id}/webhook-secret/rotateGenerates a new secret; the previous one stops working right away.
GET /crm-connections/{id}/logsThe log. Filters: status and direction.
POST /crm-connections/{id}/logs/{log_id}/retrySends a failed entry with its full payload again.
POST /crm-connections/{id}/test

cURL

curl -X POST https://api.postal.so/crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/test \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/test', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/test",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->post('crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/test', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Response
{
  "object": "crm_connection_test",
  "ok": true,
  "message": "Connected to HubSpot (12 contacts).",
  "status_code": 200
}

When the CRM refuses, ok is false and message carries the reason. fields, stages and a sync that cannot reach the CRM answer 502 crm_request_failed with a readable message.

POST /crm-connections/{id}/sync

cURL

curl -X POST https://api.postal.so/crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/sync \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/sync', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/sync",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->post('crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/sync', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
202Response
{
  "object": "crm_connection",
  "id": "019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
  "sync": "queued"
}
GET /crm-connections/{id}/logs

cURL

curl 'https://api.postal.so/crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/logs?status=failed' \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/logs?status=failed', {
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.get(
    "https://api.postal.so/crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/logs",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    params={
        "status": "failed",
    },
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->get('crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/logs', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'query' => [
        'status' => 'failed',
    ],
]);

$data = json_decode($response->getBody(), true);

#Limits

WhatLimit
Connectionsthe plan's crm_connections (and app_integrations for app ones)
Body of a received webhook256 KB (413 payload_too_large)
People per received webhook500 (any beyond that are ignored)
Webhooks received600 per minute, per source IP
Field / stage mappings100 / 100
Sources to skip (skip_sources)20
Stored sample payload (sample_payload)32 KB
Timeout of calls to the CRM10 seconds, without following redirects
Payload stored in the log16 KB (received webhooks are kept whole, up to 256 KB)
Pages read from the CRM in a sync1,000 (100 to 200 people each)

#Errors

StatusNameWhen it happens
401invalid_signatureThe webhook signature or token is missing or wrong, or the signature timestamp is more than 5 minutes old.
401restricted_api_keyA sending-only key tried to use /crm-connections.
403plan_limit_reachedThe plan does not include CRM connections or has used them all.
404not_foundThe connection does not exist in this team, or the webhook ID belongs to another connection.
409connection_pausedThe connection is paused, or only sends (does not receive from the CRM).
413payload_too_largeThe webhook body is over 256 KB.
422validation_errorInvalid credentials or settings: unknown direction, invalid Postal field or path, a property that does not exist, an unknown event, a URL that is not HTTPS, a webhook body that is not JSON; or a retry of an entry that is not eligible.
502crm_request_failedThe CRM refused Postal's call or did not respond (fields, stages and sync).

The error format and the other names are in Errors.

#See also